From c422c0d1f6bd725e5475b1ea404814c1201e3209 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 18 Oct 2023 09:41:47 -0500 Subject: [PATCH 01/41] Update setup.py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 16aa80568..332f8fc18 100644 --- a/setup.py +++ b/setup.py @@ -5,18 +5,18 @@ with open("README.md", "r", encoding="utf-8") as f: setup( name="slither-analyzer", - description="Slither is a Solidity static analysis framework written in Python 3.", + description="Slither is a Solidity and Vyper static analysis framework written in Python 3.", url="https://github.com/crytic/slither", author="Trail of Bits", - version="0.9.6", + version="0.10.0", packages=find_packages(), python_requires=">=3.8", install_requires=[ "packaging", "prettytable>=3.3.0", "pycryptodome>=3.4.6", - # "crytic-compile>=0.3.1,<0.4.0", - "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", + "crytic-compile>=0.3.5,<0.4.0", + # "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile", "web3>=6.0.0", "eth-abi>=4.0.0", "eth-typing>=3.0.0", From cd3f2d41850c5744335e9592bd491a65d1bea5de Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 18 Oct 2023 14:07:10 -0500 Subject: [PATCH 02/41] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d0a7d67be..6de5ec2c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,6 @@ ENV PATH="/home/slither/.local/bin:${PATH}" RUN --mount=type=bind,target=/mnt,source=/wheels,from=python-wheels \ pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt --no-deps /mnt/*.whl -RUN solc-select install 0.4.25 && solc-select use 0.4.25 +RUN solc-select use latest --always-install CMD /bin/bash From ea708cda4f950434804ae05d3cee02146de1f65f Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Fri, 20 Oct 2023 08:52:39 -0500 Subject: [PATCH 03/41] remove unused files --- slither/core/children/__init__.py | 0 slither/core/children/child_event.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 slither/core/children/__init__.py delete mode 100644 slither/core/children/child_event.py diff --git a/slither/core/children/__init__.py b/slither/core/children/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/slither/core/children/child_event.py b/slither/core/children/child_event.py deleted file mode 100644 index e69de29bb..000000000 From 8d9199ed6cd6498b6c0fe9229c02a78ccd9e64fc Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 18 Oct 2023 14:14:05 -0500 Subject: [PATCH 04/41] Update installation instrucitons --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f548d6b8..6ab019b77 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,14 @@ If you're **not** going to use one of the [supported compilation frameworks](htt ### Using Pip ```console -pip3 install slither-analyzer +python3 -m pip install slither-analyzer ``` ### Using Git ```bash git clone https://github.com/crytic/slither.git && cd slither -python3 setup.py install +python3 -m pip install . ``` We recommend using a Python virtual environment, as detailed in the [Developer Installation Instructions](https://github.com/trailofbits/slither/wiki/Developer-installation), if you prefer to install Slither via git. From 112786f890123497b6968480246575c8f41043ad Mon Sep 17 00:00:00 2001 From: Amos Dominion <121090896+VIELITE@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:54:10 -0400 Subject: [PATCH 05/41] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f548d6b8..c4b445783 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ * Correctly parses 99.9% of all public Solidity code * Average execution time of less than 1 second per contract * Integrates with Github's code scanning in [CI](https://github.com/marketplace/actions/slither-action) -* Support for Vyper +* Support for Vyper smart contracts ## Usage From 0cd9efec6fdf65e1867805f8ae7d8df26a03496b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:03:33 +0000 Subject: [PATCH 06/41] Bump actions/setup-node from 3 to 4 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb8f0ea6e..1660a25e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: pip install ".[test]" - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16' cache: 'npm' From b7ba2c48da01eded38b20702756d50d2a13e18c7 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 1 Nov 2023 12:12:24 -0500 Subject: [PATCH 07/41] fix is_reentrant for internal vyper functions --- slither/core/declarations/function.py | 13 +++++++++---- tests/unit/core/test_function_declaration.py | 9 ++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index e803154d0..d2baaf7e7 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -1500,10 +1500,13 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu """ Determine if the function can be re-entered """ + reentrancy_modifier = "nonReentrant" + + if self.function_language == FunctionLanguage.Vyper: + reentrancy_modifier = "nonreentrant(lock)" + # TODO: compare with hash of known nonReentrant modifier instead of the name - if "nonReentrant" in [m.name for m in self.modifiers] or "nonreentrant(lock)" in [ - m.name for m in self.modifiers - ]: + if reentrancy_modifier in [m.name for m in self.modifiers]: return False if self.visibility in ["public", "external"]: @@ -1515,7 +1518,9 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu ] if not all_entry_points: return True - return not all(("nonReentrant" in [m.name for m in f.modifiers] for f in all_entry_points)) + return not all( + (reentrancy_modifier in [m.name for m in f.modifiers] for f in all_entry_points) + ) # endregion ################################################################################### diff --git a/tests/unit/core/test_function_declaration.py b/tests/unit/core/test_function_declaration.py index cea207613..f75198d24 100644 --- a/tests/unit/core/test_function_declaration.py +++ b/tests/unit/core/test_function_declaration.py @@ -324,6 +324,9 @@ def withdraw(): @external @nonreentrant("lock") def withdraw_locked(): + self.withdraw_locked_internal() +@internal +def withdraw_locked_internal(): raw_call(msg.sender, b"", value= self.balances[msg.sender]) @payable @external @@ -376,10 +379,14 @@ def __default__(): assert not f.is_empty f = functions["withdraw_locked()"] - assert not f.is_reentrant + assert f.is_reentrant is False assert f.is_implemented assert not f.is_empty + f = functions["withdraw_locked_internal()"] + assert f.is_reentrant is False + assert f.visibility == "internal" + var = contract.get_state_variable_from_name("balances") assert var assert var.solidity_signature == "balances(address)" From 912d6b2e4e21a770437abbb5ac9e260f50ffa873 Mon Sep 17 00:00:00 2001 From: Usmann Khan Date: Thu, 16 Nov 2023 14:16:26 -0800 Subject: [PATCH 08/41] Error when a missing contract is specified to read-storage. Previously this just silently exited --- slither/tools/read_storage/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/slither/tools/read_storage/__main__.py b/slither/tools/read_storage/__main__.py index 8415ae185..3baa5d351 100644 --- a/slither/tools/read_storage/__main__.py +++ b/slither/tools/read_storage/__main__.py @@ -7,6 +7,7 @@ import argparse from crytic_compile import cryticparser from slither import Slither +from slither.exceptions import SlitherError from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo @@ -129,6 +130,8 @@ def main() -> None: if args.contract_name: contracts = slither.get_contract_from_name(args.contract_name) + if len(contracts) == 0: + raise SlitherError(f"Contract {args.contract_name} not found.") else: contracts = slither.contracts From 80b624ccf65c683d593c750cef9ab3825a27b09a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:16 +0000 Subject: [PATCH 09/41] Bump cachix/install-nix-action from 23 to 24 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 23 to 24. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v23...v24) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f04436bd3..4a646bb42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v23 + uses: cachix/install-nix-action@v24 - name: Set up cachix if: matrix.type == 'dapp' From 4673fdae7f74a384d4f95a84ea67380bf69396b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:18 +0000 Subject: [PATCH 10/41] Bump actions/configure-pages from 3 to 4 Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 3 to 4. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 29356c0c6..d92e5b091 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - uses: actions/setup-python@v4 with: python-version: '3.8' From 11def5d607b2a5aee7f7b15484079c1d7f9a39e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:22 +0000 Subject: [PATCH 11/41] Bump actions/deploy-pages from 2 to 3 Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 2 to 3. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 29356c0c6..eca7ef0b2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,4 +43,4 @@ jobs: path: './html/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v3 From 9b74e214eb6e8cdca21bb45f009c0fe47e912178 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:43:26 +0000 Subject: [PATCH 12/41] Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.10 to 1.8.11. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.10...v1.8.11) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7b4d61e89..e0ab4b543 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,7 +44,7 @@ jobs: path: dist/ - name: publish - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 - name: sign uses: sigstore/gh-action-sigstore-python@v2.1.0 From ff0e85d2c11203985924557ba848c861fc30cc3d Mon Sep 17 00:00:00 2001 From: Atreay Kukanur <66585295+ATREAY@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:12:48 +0000 Subject: [PATCH 13/41] Substituted the letter 'z' with 'x' in pre-declaration --- slither/detectors/variables/predeclaration_usage_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/variables/predeclaration_usage_local.py b/slither/detectors/variables/predeclaration_usage_local.py index b4d75e51a..9816dd6e2 100644 --- a/slither/detectors/variables/predeclaration_usage_local.py +++ b/slither/detectors/variables/predeclaration_usage_local.py @@ -36,7 +36,7 @@ class PredeclarationUsageLocal(AbstractDetector): ```solidity contract C { function f(uint z) public returns (uint) { - uint y = x + 9 + z; // 'z' is used pre-declaration + uint y = x + 9 + z; // 'x' is used pre-declaration uint x = 7; if (z % 2 == 0) { From a0365982add762e2bb4abb80cdea9b272bcddd61 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 2 Jan 2024 09:20:59 -0500 Subject: [PATCH 14/41] updated mutator --- slither/tools/mutator/__main__.py | 81 +++++++++++++++---- slither/tools/mutator/mutators/MIA.py | 73 +++++++++++------ slither/tools/mutator/mutators/MVIE.py | 45 ++++++----- slither/tools/mutator/mutators/MVIV.py | 45 ++++++----- .../mutator/mutators/abstract_mutator.py | 19 +++-- slither/tools/mutator/utils/command_line.py | 2 - slither/tools/mutator/utils/file_handling.py | 77 ++++++++++++++++++ .../tools/mutator/utils/generic_patching.py | 30 ++++--- .../tools/mutator/utils/replace_conditions.py | 43 ++++++++++ .../mutator/utils/testing_generated_mutant.py | 31 +++++++ 10 files changed, 349 insertions(+), 97 deletions(-) create mode 100644 slither/tools/mutator/utils/file_handling.py create mode 100644 slither/tools/mutator/utils/replace_conditions.py create mode 100644 slither/tools/mutator/utils/testing_generated_mutant.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 84286ce66..fde87e5e2 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,7 +2,8 @@ import argparse import inspect import logging import sys -from typing import Type, List, Any +from typing import Type, List, Any, Dict, Tuple +import os from crytic_compile import cryticparser @@ -10,9 +11,10 @@ from slither import Slither from slither.tools.mutator.mutators import all_mutators from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators +from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list logging.basicConfig() -logger = logging.getLogger("Slither") +logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) @@ -22,7 +24,6 @@ logger.setLevel(logging.INFO) ################################################################################### ################################################################################### - def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", @@ -39,6 +40,22 @@ def parse_args() -> argparse.Namespace: default=False, ) + parser.add_argument( + "--test-cmd", + help="Command line needed to run the tests for your project" + ) + + parser.add_argument( + "--test-dir", + help="Directory of tests" + ) + + # parameter to ignore the interfaces, libraries + parser.add_argument( + "--ignore-dirs", + help="Directories to ignore" + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -73,16 +90,52 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def main() -> None: - args = parse_args() - - print(args.codebase) - sl = Slither(args.codebase, **vars(args)) - - for compilation_unit in sl.compilation_units: - for M in _get_mutators(): - m = M(compilation_unit) - m.mutate() - - + # print(os.path.isdir(args.codebase)) # provided file/folder + + # arguments + test_command: str = args.test_cmd + test_directory: str = args.test_dir + paths_to_ignore: List[str] = args.ignore_dirs + + # get all the contracts as a list from given codebase + sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) + + print("Starting Mutation Campaign in", args.codebase, "\n") + for filename in sol_file_list: + # slither object + sl = Slither(filename, **vars(args)) + + # folder where backup files and valid mutants created + output_folder = os.getcwd() + "/mutation_campaign" + + # create a backup files + files_dict = backup_source_file(sl.source_code, output_folder) + + # total count of valid mutants + total_count = 0 + + # mutation + try: + for compilation_unit_of_main_file in sl.compilation_units: + # compilation_unit_of_main_file = sl.compilation_units[-1] + # for i in compilation_unit_of_main_file.contracts: + # print(i.name) + for M in _get_mutators(): + m = M(compilation_unit_of_main_file) + count = m.mutate(test_command, test_directory) + if count != None: + total_count = total_count + count + except Exception as e: + logger.error(e) + + # transfer and delete the backup files + transfer_and_delete(files_dict) + + # output + print(f"Done mutating, '{filename}'") + print(f"Valid mutant count: '{total_count}'\n") + + print("Finished Mutation Campaign in", args.codebase, "\n") # endregion + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 405888f8b..370f419c1 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,39 +1,60 @@ -from typing import Dict - +from typing import Dict, Tuple from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass - +from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite +from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file_specific_line +from slither.tools.mutator.utils.file_handling import create_mutant_file class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} - + for contract in self.slither.contracts: - - for function in contract.functions_declared + list(contract.modifiers_declared): - - for node in function.nodes: - if node.type == NodeType.IF: - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] - - # Replace the expression with true - new_str = "true" - - create_patch(result, in_file, start, stop, old_str, new_str) - - return result + if not contract.is_library: + if not contract.is_interface: + for function in contract.functions_declared + list(contract.modifiers_declared): + for node in function.nodes: + if node.contains_if(): + # print(node.expression) + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + # old_str = in_file_str[start:stop] + old_str = str(node.expression) + line_no = node.source_mapping.lines + print(line_no) + # Replace the expression with true + new_str = "true" + + replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) + + # compile and run tests + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # print(True) + # generate the mutant and patch + create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) + create_patch(result, in_file, start, stop, old_str, new_str) + + + + return (result, self.VALID_MUTANTS_COUNT) + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index a16a8252e..9dccc9f13 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,36 +1,41 @@ -from typing import Dict +from typing import Dict, Tuple from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement - +from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - return result + if not contract.is_library: + if not contract.is_interface: + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + + return (result, self.VALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index d4a7c5486..9e7b143ae 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,37 +1,42 @@ -from typing import Dict +from typing import Dict, Tuple from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement - +from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing + VALID_MUTANTS_COUNT = 1 - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - return result + if not contract.is_library: + if not contract.is_interface: + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + return (result, self.VALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 169d8725e..ab295e295 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -4,9 +4,10 @@ from enum import Enum from typing import Optional, Dict from slither.core.compilation_unit import SlitherCompilationUnit +from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff -logger = logging.getLogger("Slither") +logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): @@ -68,13 +69,14 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- ) @abc.abstractmethod - def _mutate(self) -> Dict: + def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self) -> None: - all_patches = self._mutate() - + def mutate(self, testing_command: str, testing_directory: str) -> int: + # call _mutate function from different mutators + (all_patches, valid_mutant_count) = self._mutate(testing_command, testing_directory) + if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") return @@ -93,4 +95,11 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") + # print the differences print(diff) + + return valid_mutant_count + + + + \ No newline at end of file diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index feb479c5c..80d610a69 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -1,9 +1,7 @@ from typing import List, Type - from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.utils.myprettytable import MyPrettyTable - def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: mutators_list = [] for detector in mutators_classes: diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py new file mode 100644 index 000000000..800232a5a --- /dev/null +++ b/slither/tools/mutator/utils/file_handling.py @@ -0,0 +1,77 @@ +import os +from typing import Dict, Tuple, List +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to backup the source file +def backup_source_file(source_code: Dict, output_folder: str) -> Dict: + duplicated_files = {} + os.makedirs(output_folder, exist_ok=True) + + for file_path, content in source_code.items(): + directory, filename = os.path.split(file_path) + new_filename = f"{output_folder}/backup_{filename}" + new_file_path = os.path.join(directory, new_filename) + + with open(new_file_path, 'w') as new_file: + new_file.write(content) + duplicated_files[file_path] = new_file_path + + return duplicated_files + +# function to transfer the original content to the sol file after campaign +def transfer_and_delete(files_dict: Dict) -> None: + try: + for item, value in files_dict.items(): + with open(value, 'r') as duplicated_file: + content = duplicated_file.read() + + with open(item, 'w') as original_file: + original_file.write(content) + + os.remove(value) + except Exception as e: + logger.error(f"Error transferring content: {e}") + +#function to create new mutant file +def create_mutant_file(file: str, count: int, rule: str) -> None: + try: + directory, filename = os.path.split(file) + # Read content from the duplicated file + with open(file, 'r') as source_file: + content = source_file.read() + + # Write content to the original file + mutant_name = filename.split('.')[0] + with open("mutation_campaign/" + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + mutant_file.write(content) + + except Exception as e: + logger.error(f"Error creating mutant: {e}") + +# function to get the contracts list +def get_sol_file_list(codebase: str, ignore_paths: List[str]) -> List[str]: + sol_file_list = [] + + # if input is contract file + if os.path.isfile(codebase): + return [codebase] + + # if input is folder + elif os.path.isdir(codebase): + directory = os.path.abspath(codebase) + for file in os.listdir(directory): + filename = os.path.join(directory, file) + if os.path.isfile(filename): + sol_file_list.append(filename) + elif os.path.isdir(filename): + directory_name, dirname = os.path.split(filename) + if dirname in ignore_paths: + continue + for i in get_sol_file_list(filename, ignore_paths): + sol_file_list.append(i) + + return sol_file_list +# to_do: create a function to delete the commands from the sol file +# def remove_comments(self) -> None: \ No newline at end of file diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py index d773ea784..03ccadec7 100644 --- a/slither/tools/mutator/utils/generic_patching.py +++ b/slither/tools/mutator/utils/generic_patching.py @@ -1,11 +1,14 @@ from typing import Dict +import os from slither.core.declarations import Contract from slither.core.variables.variable import Variable from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite +from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file +from slither.tools.mutator.utils.file_handling import create_mutant_file - -def remove_assignement(variable: Variable, contract: Contract, result: Dict): +def remove_assignement(variable: Variable, contract: Contract, result: Dict, test_cmd: str, test_dir: str) -> bool: """ Remove the variable's initial assignement @@ -25,12 +28,19 @@ def remove_assignement(variable: Variable, contract: Contract, result: Dict): old_str = in_file_str[start:stop] new_str = old_str[: old_str.find("=")] + + replace_string_in_source_file(in_file, in_file_str[variable.source_mapping.start + old_str.find("="):variable.source_mapping.end], '') - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - ) + # compile and run tests before the mutant generated before patching + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # create_mutant_file(in_file, ) + create_patch( + result, + in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + ) + return True \ No newline at end of file diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py new file mode 100644 index 000000000..82d6c11c9 --- /dev/null +++ b/slither/tools/mutator/utils/replace_conditions.py @@ -0,0 +1,43 @@ +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to replace the string +def replace_string_in_source_file(file_path: str, old_string: str, new_string: str) -> None: + try: + # Read the content of the Solidity file + with open(file_path, 'r') as file: + content = file.read() + + # Perform the string replacement + modified_content = content.replace(old_string, new_string) + + # Write the modified content back to the file + with open(file_path, 'w') as file: + file.write(modified_content) + + logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.") + except Exception as e: + logger.error(f"Error replacing string: {e}") + +# function to replace the string in a specific line +def replace_string_in_source_file_specific_line(file_path: str, old_string: str, new_string: str, line_number : int) -> None: + try: + # Read the content of the Solidity file + with open(file_path, 'r') as file: + lines = file.readlines() + + if 1 <= line_number <= len(lines): + # Replace the old string with the new string on the specified line + lines[line_number - 1] = lines[line_number - 1].replace(old_string, new_string) + + # Write the modified content back to the file + with open(file_path, 'w') as file: + file.writelines(lines) + + logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.' at '{line_number}") + else: + logger.error(f'Error: Line number {line_number} is out of range') + + except Exception as e: + logger.erro(f'Error: {e}') \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py new file mode 100644 index 000000000..1c7de9acf --- /dev/null +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -0,0 +1,31 @@ +import crytic_compile +import subprocess +import os +import logging + +logger = logging.getLogger("Slither-Mutate") + +# function to compile the generated mutant +def compile_generated_mutant(file_path: str) -> bool: + try: + crytic_compile.CryticCompile(file_path) + return True + except Exception as e: # pylint: disable=broad-except + logger.error("Error Crytic Compile", e) + +# function to run the tests +def run_test_suite(cmd: str, dir: str) -> bool: + try: + # Change to the foundry folder + # os.chdir(dir) + + result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if not result.stderr: + return True + except subprocess.CalledProcessError as e: + logger.error(f"Error executing 'forge test': {e}") + return False + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return False \ No newline at end of file From 37c23e11ce63f229593a500e7a1d8512d00cce43 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 3 Jan 2024 11:35:08 -0500 Subject: [PATCH 15/41] Updated replace string logic --- slither/tools/mutator/__main__.py | 31 +++++--- slither/tools/mutator/mutators/MIA.py | 72 ++++++++++--------- slither/tools/mutator/mutators/MVIE.py | 44 ++++++------ slither/tools/mutator/mutators/MVIV.py | 44 ++++++------ .../mutator/mutators/abstract_mutator.py | 8 +-- .../tools/mutator/mutators/all_mutators.py | 4 +- slither/tools/mutator/utils/file_handling.py | 10 ++- .../tools/mutator/utils/replace_conditions.py | 9 ++- .../mutator/utils/testing_generated_mutant.py | 7 +- 9 files changed, 131 insertions(+), 98 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index fde87e5e2..9cb1a9f8d 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -40,11 +40,13 @@ def parse_args() -> argparse.Namespace: default=False, ) + # argument to add the test command parser.add_argument( "--test-cmd", help="Command line needed to run the tests for your project" ) + # argument to add the test directory - containing all the tests parser.add_argument( "--test-dir", help="Directory of tests" @@ -96,13 +98,14 @@ def main() -> None: # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir - paths_to_ignore: List[str] = args.ignore_dirs + paths_to_ignore: List[str] | None = args.ignore_dirs # get all the contracts as a list from given codebase sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) print("Starting Mutation Campaign in", args.codebase, "\n") for filename in sol_file_list: + contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) @@ -112,9 +115,12 @@ def main() -> None: # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) - # total count of valid mutants + # total count of mutants total_count = 0 - + + # count of valid mutants + v_count = 0 + # mutation try: for compilation_unit_of_main_file in sl.compilation_units: @@ -123,19 +129,24 @@ def main() -> None: # print(i.name) for M in _get_mutators(): m = M(compilation_unit_of_main_file) - count = m.mutate(test_command, test_directory) - if count != None: - total_count = total_count + count + v_count, i_count = m.mutate(test_command, test_directory, contract_name) + if v_count != None and i_count != None: + total_count = total_count + v_count + i_count except Exception as e: logger.error(e) + except KeyboardInterrupt: + # transfer and delete the backup files if interrupted + logger.error("\nExecution interrupted by user (Ctrl + C). Cleaning up...") + transfer_and_delete(files_dict) + # transfer and delete the backup files transfer_and_delete(files_dict) - - # output - print(f"Done mutating, '{filename}'") - print(f"Valid mutant count: '{total_count}'\n") + + # output + print(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n") + print("Finished Mutation Campaign in", args.codebase, "\n") # endregion \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 370f419c1..b1c55d184 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -11,47 +11,53 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - for function in contract.functions_declared + list(contract.modifiers_declared): - for node in function.nodes: - if node.contains_if(): - # print(node.expression) - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + for function in contract.functions_declared + list(contract.modifiers_declared): + for node in function.nodes: + if node.contains_if(): + # print(node.expression) + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - # old_str = in_file_str[start:stop] - old_str = str(node.expression) - line_no = node.source_mapping.lines - print(line_no) - # Replace the expression with true - new_str = "true" - - replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) - - # compile and run tests - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # print(True) - # generate the mutant and patch - create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) - create_patch(result, in_file, start, stop, old_str, new_str) - + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + # old_str = in_file_str[start:stop] + old_str = str(node.expression) + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = "true" + print(line_no[0]) + replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) + + # compile and run tests + if compile_generated_mutant(in_file): + if run_test_suite(test_cmd, test_dir): + # generate the mutant and patch + create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) + create_patch(result, in_file, start, stop, old_str, new_str) + self.VALID_MUTANTS_COUNT = self.VALID_MUTANTS_COUNT + 1 + else: + self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 + else: + self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 + print(self.INVALID_MUTANTS_COUNT) + - return (result, self.VALID_MUTANTS_COUNT) + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 9dccc9f13..1c99b24ee 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -11,31 +11,33 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - if not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + if not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + - return (result, self.VALID_MUTANTS_COUNT) + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 9e7b143ae..bb1c4cabe 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -11,32 +11,34 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 1 + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str) -> Tuple[(Dict, int)]: + def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: result: Dict = {} variable: Variable for contract in self.slither.contracts: - if not contract.is_library: - if not contract.is_interface: - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + # if not contract.is_library: + # if not contract.is_interface: + if contract_name == str(contract.name): + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - if isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + if isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - return (result, self.VALID_MUTANTS_COUNT) + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if(remove_assignement(variable, contract, result, test_cmd, test_dir)): + create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) + + return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ab295e295..a79f45906 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,7 +1,7 @@ import abc import logging from enum import Enum -from typing import Optional, Dict +from typing import Optional, Dict, Tuple from slither.core.compilation_unit import SlitherCompilationUnit from slither.tools.doctor.utils import snip_section @@ -73,9 +73,9 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str) -> int: + def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[(int, int)]: # call _mutate function from different mutators - (all_patches, valid_mutant_count) = self._mutate(testing_command, testing_directory) + (all_patches, valid_mutant_count, invalid_mutant_count) = self._mutate(testing_command, testing_directory, contract_name) if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") @@ -98,7 +98,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # print the differences print(diff) - return valid_mutant_count + return (valid_mutant_count, invalid_mutant_count) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 5508fb68e..5a9465c86 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,4 +1,4 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV -from slither.tools.mutator.mutators.MVIE import MVIE +# from slither.tools.mutator.mutators.MVIV import MVIV +# from slither.tools.mutator.mutators.MVIE import MVIE from slither.tools.mutator.mutators.MIA import MIA diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 800232a5a..c041f76d6 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -36,6 +36,7 @@ def transfer_and_delete(files_dict: Dict) -> None: #function to create new mutant file def create_mutant_file(file: str, count: int, rule: str) -> None: + try: directory, filename = os.path.split(file) # Read content from the duplicated file @@ -44,16 +45,19 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # Write content to the original file mutant_name = filename.split('.')[0] - with open("mutation_campaign/" + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + # create folder for each contract + os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) + with open("mutation_campaign/" + mutant_name + '/' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: mutant_file.write(content) except Exception as e: logger.error(f"Error creating mutant: {e}") # function to get the contracts list -def get_sol_file_list(codebase: str, ignore_paths: List[str]) -> List[str]: +def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: sol_file_list = [] - + if ignore_paths == None: + ignore_paths = [] # if input is contract file if os.path.isfile(codebase): return [codebase] diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py index 82d6c11c9..4e3f91454 100644 --- a/slither/tools/mutator/utils/replace_conditions.py +++ b/slither/tools/mutator/utils/replace_conditions.py @@ -1,4 +1,5 @@ import logging +import re logger = logging.getLogger("Slither-Mutate") @@ -8,7 +9,7 @@ def replace_string_in_source_file(file_path: str, old_string: str, new_string: s # Read the content of the Solidity file with open(file_path, 'r') as file: content = file.read() - + # Perform the string replacement modified_content = content.replace(old_string, new_string) @@ -28,8 +29,12 @@ def replace_string_in_source_file_specific_line(file_path: str, old_string: str, lines = file.readlines() if 1 <= line_number <= len(lines): + # remove the spaces in the string + line = lines[line_number - 1].replace(" ", "") + old_string = old_string.replace(" ", "") + # Replace the old string with the new string on the specified line - lines[line_number - 1] = lines[line_number - 1].replace(old_string, new_string) + lines[line_number - 1] = line.replace(old_string.strip(), new_string) # Write the modified content back to the file with open(file_path, 'w') as file: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 1c7de9acf..61b9db214 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -20,11 +20,14 @@ def run_test_suite(cmd: str, dir: str) -> bool: # os.chdir(dir) result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + # result = subprocess.run(cmd.split(' '), check=True) + print(result.stdout) if not result.stderr: return True except subprocess.CalledProcessError as e: - logger.error(f"Error executing 'forge test': {e}") + print(e.output) + logger.error(f"Error executing '{cmd}': {e}") + return False except Exception as e: logger.error(f"An unexpected error occurred: {e}") From 36eda1b3cb8599033097e613750eb7db154c5b55 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 5 Jan 2024 19:31:25 -0500 Subject: [PATCH 16/41] Added new mutant generators --- slither/tools/mutator/__main__.py | 44 ++++++--- slither/tools/mutator/mutators/LOR.py | 47 ++++++++++ slither/tools/mutator/mutators/MIA.py | 65 ++++--------- slither/tools/mutator/mutators/MVIE.py | 48 ++++------ slither/tools/mutator/mutators/MVIV.py | 41 ++++---- slither/tools/mutator/mutators/ROR.py | 53 +++++++++++ slither/tools/mutator/mutators/SBR.py | 94 +++++++++++++++++++ slither/tools/mutator/mutators/UOI.py | 56 +++++++++++ .../mutator/mutators/abstract_mutator.py | 46 ++++++--- .../tools/mutator/mutators/all_mutators.py | 7 +- slither/tools/mutator/utils/file_handling.py | 34 ++++++- .../tools/mutator/utils/generic_patching.py | 28 +++--- .../mutator/utils/testing_generated_mutant.py | 58 +++++++++++- 13 files changed, 467 insertions(+), 154 deletions(-) create mode 100644 slither/tools/mutator/mutators/LOR.py create mode 100644 slither/tools/mutator/mutators/ROR.py create mode 100644 slither/tools/mutator/mutators/SBR.py create mode 100644 slither/tools/mutator/mutators/UOI.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 9cb1a9f8d..201b39acd 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,6 +4,7 @@ import logging import sys from typing import Type, List, Any, Dict, Tuple import os +import shutil from crytic_compile import cryticparser @@ -12,12 +13,12 @@ from slither.tools.mutator.mutators import all_mutators from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list +from slither.utils.colors import yellow, magenta logging.basicConfig() logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) - ################################################################################### ################################################################################### # region Cli Arguments @@ -52,12 +53,17 @@ def parse_args() -> argparse.Namespace: help="Directory of tests" ) - # parameter to ignore the interfaces, libraries + # argument to ignore the interfaces, libraries parser.add_argument( "--ignore-dirs", help="Directories to ignore" ) + # to_do: add time out argument + parser.add_argument( + "--timeout", + help="Set timeout for test command" + ) # Initiate all the crytic config cli options cryticparser.init(parser) @@ -90,7 +96,6 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods ################################################################################### ################################################################################### - def main() -> None: args = parse_args() # print(os.path.isdir(args.codebase)) # provided file/folder @@ -98,19 +103,29 @@ def main() -> None: # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir - paths_to_ignore: List[str] | None = args.ignore_dirs + paths_to_ignore: str | None = args.ignore_dirs + timeout: int = args.timeout + + print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) + + if paths_to_ignore: + paths_to_ignore_list = paths_to_ignore.strip('][').split(',') + print(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) + else: + paths_to_ignore_list = [] # get all the contracts as a list from given codebase - sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore) + sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) + + # folder where backup files and valid mutants created + output_folder = os.getcwd() + "/mutation_campaign" + if os.path.exists(output_folder): + shutil.rmtree(output_folder) - print("Starting Mutation Campaign in", args.codebase, "\n") for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - - # folder where backup files and valid mutants created - output_folder = os.getcwd() + "/mutation_campaign" # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) @@ -129,9 +144,9 @@ def main() -> None: # print(i.name) for M in _get_mutators(): m = M(compilation_unit_of_main_file) - v_count, i_count = m.mutate(test_command, test_directory, contract_name) - if v_count != None and i_count != None: - total_count = total_count + v_count + i_count + count_valid, count_invalid = m.mutate(test_command, test_directory, contract_name) + v_count += count_valid + total_count += count_valid + count_invalid except Exception as e: logger.error(e) @@ -143,10 +158,9 @@ def main() -> None: # transfer and delete the backup files transfer_and_delete(files_dict) - # output - print(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n") + print(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) - print("Finished Mutation Campaign in", args.codebase, "\n") + print(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) # endregion \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py new file mode 100644 index 000000000..382a08aee --- /dev/null +++ b/slither/tools/mutator/mutators/LOR.py @@ -0,0 +1,47 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +logical_operators = [ + BinaryType.OROR, + BinaryType.ANDAND, +] + +class LOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "LOR" + HELP = "Logical operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + + contract = self.contract + + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + for function in contract.functions_and_modifiers_declared: + + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in logical_operators: + alternative_ops = logical_operators[:] + alternative_ops.remove(ir.type) + + for op in alternative_ops: + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index b1c55d184..d76369d4d 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,63 +1,38 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite -from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file_specific_line -from slither.tools.mutator.utils.file_handling import create_mutant_file class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} + # Retrieve the file + in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = self.contract.compilation_unit.core.source_code[in_file] - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - for function in contract.functions_declared + list(contract.modifiers_declared): - for node in function.nodes: - if node.contains_if(): - # print(node.expression) - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + for function in self.contract.functions_declared + list(self.contract.modifiers_declared): + for node in function.nodes: + if node.type == NodeType.IF: + + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - # old_str = in_file_str[start:stop] - old_str = str(node.expression) - line_no = node.source_mapping.lines - # Replace the expression with true - new_str = "true" - print(line_no[0]) - replace_string_in_source_file_specific_line(in_file, old_str, new_str, line_no[0]) - - # compile and run tests - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # generate the mutant and patch - create_mutant_file(in_file, self.VALID_MUTANTS_COUNT, self.NAME) - create_patch(result, in_file, start, stop, old_str, new_str) - self.VALID_MUTANTS_COUNT = self.VALID_MUTANTS_COUNT + 1 - else: - self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 - else: - self.INVALID_MUTANTS_COUNT = self.INVALID_MUTANTS_COUNT + 1 - print(self.INVALID_MUTANTS_COUNT) + # Replace the expression with true and false + for value in ["true", "false"]: + new_str = value + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) - - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return result diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 1c99b24ee..15b2a20c3 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,43 +1,35 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement -from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} variable: Variable - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue - - if not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + contract = self.contract + + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if not isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) + + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and not isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) + + return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index bb1c4cabe..5e7c0a6e1 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,41 +4,32 @@ from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass from slither.tools.mutator.utils.generic_patching import remove_assignement -from slither.tools.mutator.utils.file_handling import create_mutant_file class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing - VALID_MUTANTS_COUNT = 0 - INVALID_MUTANTS_COUNT = 0 - def _mutate(self, test_cmd: str, test_dir: str, contract_name: str) -> Tuple[(Dict, int, int)]: + def _mutate(self) -> Dict: result: Dict = {} variable: Variable + contract = self.contract + + # Create fault for state variables declaration + for variable in contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue - for contract in self.slither.contracts: - # if not contract.is_library: - # if not contract.is_interface: - if contract_name == str(contract.name): - # Create fault for state variables declaration - for variable in contract.state_variables_declared: - if variable.initialized: - # Cannot remove the initialization of constant variables - if variable.is_constant: - continue + if isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) - if isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - + for function in contract.functions_declared + list(contract.modifiers_declared): + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + remove_assignement(variable, contract, result) - for function in contract.functions_declared + list(contract.modifiers_declared): - for variable in function.local_variables: - if variable.initialized and isinstance(variable.expression, Literal): - if(remove_assignement(variable, contract, result, test_cmd, test_dir)): - create_mutant_file(contract.source_mapping.filename.absolute, self.VALID_MUTANTS_COUNT, self.NAME) - - return (result, self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return result diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py new file mode 100644 index 000000000..9a8942e19 --- /dev/null +++ b/slither/tools/mutator/mutators/ROR.py @@ -0,0 +1,53 @@ +from typing import Dict +from collections import defaultdict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + + +relational_operators = [ + BinaryType.LESS, + BinaryType.GREATER, + BinaryType.LESS_EQUAL, + BinaryType.GREATER_EQUAL, + BinaryType.EQUAL, + BinaryType.NOT_EQUAL, +] + + +class ROR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "ROR" + HELP = "Relational operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + # result["patches"] = defaultdict(list) + contract = self.contract + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + # Retrieve the file + in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = self.contract.compilation_unit.core.source_code[in_file] + + if isinstance(ir, Binary) and ir.type in relational_operators: + alternative_ops = relational_operators[:] + alternative_ops.remove(ir.type) + + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py new file mode 100644 index 000000000..ac35d9540 --- /dev/null +++ b/slither/tools/mutator/mutators/SBR.py @@ -0,0 +1,94 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +import re + +solidity_rules = [ + "abi\.encode\( ==> abi.encodePacked(", + "abi\.encodePacked\( ==> abi.encode(", + "\.call([({]) ==> .delegatecall\\1", + "\.call([({]) ==> .staticcall\\1", + "\.delegatecall([({]) ==> .call\\1", + "\.delegatecall([({]) ==> .staticcall\\1", + "\.staticcall([({]) ==> .delegatecall\\1", + "\.staticcall([({]) ==> .call\\1", + "^now$ ==> 0", + "block.timestamp ==> 0", + "msg.value ==> 0", + "msg.value ==> 1", + "(\s)(wei|gwei) ==> \\1ether", + "(\s)(ether|gwei) ==> \\1wei", + "(\s)(wei|ether) ==> \\1gwei", + "(\s)(minutes|days|hours|weeks) ==> \\1seconds", + "(\s)(seconds|days|hours|weeks) ==> \\1minutes", + "(\s)(seconds|minutes|hours|weeks) ==> \\1days", + "(\s)(seconds|minutes|days|weeks) ==> \\1hours", + "(\s)(seconds|minutes|days|hours) ==> \\1weeks", + "(\s)(memory) ==> \\1storage", + "(\s)(storage) ==> \\1memory", + "(\s)(constant) ==> \\1immutable", + "addmod ==> mulmod", + "mulmod ==> addmod", + "msg.sender ==> tx.origin", + "tx.origin ==> msg.sender", + "([^u])fixed ==> \\1ufixed", + "ufixed ==> fixed", + "(u?)int16 ==> \\1int8", + "(u?)int32 ==> \\1int16", + "(u?)int64 ==> \\1int32", + "(u?)int128 ==> \\1int64", + "(u?)int256 ==> \\1int128" +] + + +class SBR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "SBR" + HELP = 'Solidity Based Replacements' + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + contract = self.contract + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + + for variable in contract.state_variables_declared: + node = variable.node_initialization + if node: + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + line_no = node.source_mapping.lines + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOI.py new file mode 100644 index 000000000..6d5862e76 --- /dev/null +++ b/slither/tools/mutator/mutators/UOI.py @@ -0,0 +1,56 @@ +from typing import Dict +import re +from slither.core.expressions.unary_operation import UnaryOperationType +from slither.slithir.variables import Constant +from slither.core.variables.local_variable import LocalVariable +from slither.core.expressions.expression import Expression +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.cfg.node import NodeType + + +unary_operators = [ + UnaryOperationType.PLUSPLUS_PRE, + UnaryOperationType.MINUSMINUS_PRE, + UnaryOperationType.PLUSPLUS_POST, + UnaryOperationType.MINUSMINUS_POST, + UnaryOperationType.MINUS_PRE, +] + + +class UOI(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "UOI" + HELP = "Unary operator insertion" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + + result: Dict = {} + + contract = self.contract + + # Retrieve the file + in_file = contract.source_mapping.filename.absolute + # Retrieve the source code + in_file_str = contract.compilation_unit.core.source_code[in_file] + + for function in contract.functions_and_modifiers_declared: + for node in function.nodes: + if (node.type == NodeType.EXPRESSION): + for op in unary_operators: + if str(op) in str(node.expression): + for i in node.variables_written: + print(i) + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = in_file_str[start:stop] + # print(old_str) + # Replace the expression with true + # new_str = old_str.replace(str(operand), f"{str(op)}{operand}") + # new_str = re.sub(r'(\w+)\+\+', r'++\1', text) + # print(new_str) + # create_patch(result, in_file, start, stop, old_str, new_str) + print(result) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index a79f45906..e58b705a1 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,12 +1,12 @@ import abc import logging from enum import Enum -from typing import Optional, Dict, Tuple +from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff - +from slither.tools.mutator.utils.testing_generated_mutant import test_patch logger = logging.getLogger("Slither-Mutate") @@ -34,6 +34,8 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- HELP = "" FAULTCLASS = FaultClass.Undefined FAULTNATURE = FaultNature.Undefined + VALID_MUTANTS_COUNT = 0 + INVALID_MUTANTS_COUNT = 0 def __init__( self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None @@ -73,13 +75,18 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[(int, int)]: + def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[int, int]: + # identify the main contract, ignore the imports + for contract in self.slither.contracts: + if contract_name == str(contract.name): + self.contract = contract + # call _mutate function from different mutators - (all_patches, valid_mutant_count, invalid_mutant_count) = self._mutate(testing_command, testing_directory, contract_name) - + (all_patches) = self._mutate() + if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") - return + return (0,0) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -87,18 +94,27 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- offset = 0 patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): - logger.info(f"Impossible to generate patch; patches collisions: {patches}") - continue + # if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): + # logger.error(f"Impossible to generate patch; patches collisions: {patches}") + # continue for patch in patches: - patched_txt, offset = apply_patch(patched_txt, patch, offset) - diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) - if not diff: - logger.info(f"Impossible to generate patch; empty {patches}") + # print(patch) + # test the patch + flag = test_patch(file, patch, testing_command, self.VALID_MUTANTS_COUNT, self.NAME) + # count the valid and invalid mutants + if not flag: + self.INVALID_MUTANTS_COUNT += 1 + continue + self.VALID_MUTANTS_COUNT += 1 + # patched_txt, offset = apply_patch(patched_txt, patch, offset) + # diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + # if not diff: + # logger.info(f"Impossible to generate patch; empty {patches}") + # print the differences - print(diff) + # print(diff) - return (valid_mutant_count, invalid_mutant_count) + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 5a9465c86..4b0c3b1ad 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,4 +1,9 @@ # pylint: disable=unused-import # from slither.tools.mutator.mutators.MVIV import MVIV # from slither.tools.mutator.mutators.MVIE import MVIE -from slither.tools.mutator.mutators.MIA import MIA +# from slither.tools.mutator.mutators.MIA import MIA +from slither.tools.mutator.mutators.ROR import ROR +# from slither.tools.mutator.mutators.LOR import LOR +# from slither.tools.mutator.mutators.UOI import UOI +# from slither.tools.mutator.mutators.SBR import SBR + diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index c041f76d6..70f7deb11 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -4,9 +4,10 @@ import logging logger = logging.getLogger("Slither-Mutate") +duplicated_files = {} + # function to backup the source file def backup_source_file(source_code: Dict, output_folder: str) -> Dict: - duplicated_files = {} os.makedirs(output_folder, exist_ok=True) for file_path, content in source_code.items(): @@ -23,7 +24,8 @@ def backup_source_file(source_code: Dict, output_folder: str) -> Dict: # function to transfer the original content to the sol file after campaign def transfer_and_delete(files_dict: Dict) -> None: try: - for item, value in files_dict.items(): + files_dict_copy = files_dict.copy() + for item, value in files_dict_copy.items(): with open(value, 'r') as duplicated_file: content = duplicated_file.read() @@ -31,6 +33,10 @@ def transfer_and_delete(files_dict: Dict) -> None: original_file.write(content) os.remove(value) + + # delete elements from the global dict + del duplicated_files[item] + except Exception as e: logger.error(f"Error transferring content: {e}") @@ -45,14 +51,36 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # Write content to the original file mutant_name = filename.split('.')[0] + # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: mutant_file.write(content) + # reset the file + with open(duplicated_files[file], 'r') as duplicated_file: + duplicate_content = duplicated_file.read() + + with open(file, 'w') as source_file: + source_file.write(duplicate_content) + except Exception as e: logger.error(f"Error creating mutant: {e}") +# function to reset the file +def reset_file(file: str) -> None: + try: + # directory, filename = os.path.split(file) + # reset the file + with open(duplicated_files[file], 'r') as duplicated_file: + duplicate_content = duplicated_file.read() + + with open(file, 'w') as source_file: + source_file.write(duplicate_content) + + except Exception as e: + logger.error(f"Error resetting file: {e}") + # function to get the contracts list def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: sol_file_list = [] diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py index 03ccadec7..6c0ba6511 100644 --- a/slither/tools/mutator/utils/generic_patching.py +++ b/slither/tools/mutator/utils/generic_patching.py @@ -8,7 +8,7 @@ from slither.tools.mutator.utils.testing_generated_mutant import compile_generat from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file from slither.tools.mutator.utils.file_handling import create_mutant_file -def remove_assignement(variable: Variable, contract: Contract, result: Dict, test_cmd: str, test_dir: str) -> bool: +def remove_assignement(variable: Variable, contract: Contract, result: Dict) -> bool: """ Remove the variable's initial assignement @@ -28,19 +28,13 @@ def remove_assignement(variable: Variable, contract: Contract, result: Dict, tes old_str = in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - - replace_string_in_source_file(in_file, in_file_str[variable.source_mapping.start + old_str.find("="):variable.source_mapping.end], '') - - # compile and run tests before the mutant generated before patching - if compile_generated_mutant(in_file): - if run_test_suite(test_cmd, test_dir): - # create_mutant_file(in_file, ) - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - ) - return True \ No newline at end of file + line_no = [0] + create_patch( + result, + in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 61b9db214..7ab406519 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -2,8 +2,13 @@ import crytic_compile import subprocess import os import logging - +import time +import signal +from typing import List, Dict +from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file +from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") +timeout = 30 # we can get it as parameter # function to compile the generated mutant def compile_generated_mutant(file_path: str) -> bool: @@ -11,7 +16,9 @@ def compile_generated_mutant(file_path: str) -> bool: crytic_compile.CryticCompile(file_path) return True except Exception as e: # pylint: disable=broad-except - logger.error("Error Crytic Compile", e) + print(True) + # logger.error("Error Crytic Compile") + return False # function to run the tests def run_test_suite(cmd: str, dir: str) -> bool: @@ -21,14 +28,55 @@ def run_test_suite(cmd: str, dir: str) -> bool: result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # result = subprocess.run(cmd.split(' '), check=True) - print(result.stdout) if not result.stderr: return True except subprocess.CalledProcessError as e: - print(e.output) logger.error(f"Error executing '{cmd}': {e}") return False except Exception as e: logger.error(f"An unexpected error occurred: {e}") - return False \ No newline at end of file + return False + +def run_test_cmd(cmd: str, dir: str) -> bool: + start = time.time() + + # starting new process + P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) + + try: + # checking whether the process is completed or not for 30 seconds + while P.poll() is None and (time.time() - start) < timeout: + time.sleep(0.05) + finally: + if P.poll() is None: + print() + print("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + # sends a SIGTERM signal to process group - bascially killing the process + os.killpg(os.getpgid(P.pid), signal.SIGTERM) + # Avoid any weird race conditions from grabbing the return code + time.sleep(0.05) + # indicates whether the command executed sucessfully or not + r = P.returncode + + # if r is 0 then it is valid mutant because tests didn't fail + return True if r == 0 else False + +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str) -> bool: + with open(file, 'r') as filepath: + content = filepath.read() + # Perform the replacement based on the index values + replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] + + # Write the modified content back to the file + with open(file, 'w') as filepath: + filepath.write(replaced_content) + if(compile_generated_mutant(file)): + if(run_test_cmd(command, file)): + create_mutant_file(file, index, generator_name) + logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) + return True + + reset_file(file) + logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> INVALID\n")) + return False \ No newline at end of file From 67b95dff7440c807e168c4796c37948810d0d77a Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 9 Jan 2024 12:03:22 -0500 Subject: [PATCH 17/41] Added new mutators --- slither/tools/mutator/__main__.py | 33 ++++++---- slither/tools/mutator/mutators/AOR.py | 38 ++++++++++++ slither/tools/mutator/mutators/ASOR.py | 50 +++++++++++++++ slither/tools/mutator/mutators/BOR.py | 35 +++++++++++ slither/tools/mutator/mutators/LOR.py | 14 +---- slither/tools/mutator/mutators/MIA.py | 20 +++--- slither/tools/mutator/mutators/MVIE.py | 45 +++++++++++--- slither/tools/mutator/mutators/MVIV.py | 45 +++++++++++--- slither/tools/mutator/mutators/MWA.py | 34 +++++++++++ slither/tools/mutator/mutators/ROR.py | 20 ++---- slither/tools/mutator/mutators/SBR.py | 22 +++---- slither/tools/mutator/mutators/UOI.py | 61 +++++++++---------- .../mutator/mutators/abstract_mutator.py | 25 +++++--- .../tools/mutator/mutators/all_mutators.py | 16 +++-- .../tools/mutator/utils/generic_patching.py | 40 ------------ .../tools/mutator/utils/replace_conditions.py | 48 --------------- .../mutator/utils/testing_generated_mutant.py | 13 ++-- 17 files changed, 339 insertions(+), 220 deletions(-) create mode 100644 slither/tools/mutator/mutators/AOR.py create mode 100644 slither/tools/mutator/mutators/ASOR.py create mode 100644 slither/tools/mutator/mutators/BOR.py create mode 100644 slither/tools/mutator/mutators/MWA.py delete mode 100644 slither/tools/mutator/utils/generic_patching.py delete mode 100644 slither/tools/mutator/utils/replace_conditions.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 201b39acd..aba427355 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -59,11 +59,18 @@ def parse_args() -> argparse.Namespace: help="Directories to ignore" ) - # to_do: add time out argument + # time out argument parser.add_argument( "--timeout", - help="Set timeout for test command" + help="Set timeout for test command (by deafult 30 seconds)" ) + + # output directory argument + parser.add_argument( + "--output-dir", + help="Output Directory (by default it is 'mutation_campaign')" + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -98,13 +105,13 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def main() -> None: args = parse_args() - # print(os.path.isdir(args.codebase)) # provided file/folder # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir paths_to_ignore: str | None = args.ignore_dirs - timeout: int = args.timeout + output_dir: str | None = args.output_dir + timeout: int | None = args.timeout print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -118,33 +125,33 @@ def main() -> None: sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) # folder where backup files and valid mutants created - output_folder = os.getcwd() + "/mutation_campaign" + if output_dir == None: + output_dir = "/mutation_campaign" + output_folder = os.getcwd() + output_dir if os.path.exists(output_folder): shutil.rmtree(output_folder) + # set default timeout + if timeout == None: + timeout = 30 + for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) - # total count of mutants total_count = 0 - # count of valid mutants v_count = 0 # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - # compilation_unit_of_main_file = sl.compilation_units[-1] - # for i in compilation_unit_of_main_file.contracts: - # print(i.name) for M in _get_mutators(): - m = M(compilation_unit_of_main_file) - count_valid, count_invalid = m.mutate(test_command, test_directory, contract_name) + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name) + count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid except Exception as e: diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py new file mode 100644 index 000000000..f248d3c77 --- /dev/null +++ b/slither/tools/mutator/mutators/AOR.py @@ -0,0 +1,38 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +arithmetic_operators = [ + BinaryType.ADDITION, + BinaryType.DIVISION, + BinaryType.MULTIPLICATION, + BinaryType.SUBTRACTION, + BinaryType.MODULO +] + +class AOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "AOR" + HELP = "Arithmetic operator replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in arithmetic_operators: + alternative_ops = arithmetic_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py new file mode 100644 index 000000000..8c70756a2 --- /dev/null +++ b/slither/tools/mutator/mutators/ASOR.py @@ -0,0 +1,50 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation + +assignment_operators = [ + AssignmentOperationType.ASSIGN_ADDITION, + AssignmentOperationType.ASSIGN_SUBTRACTION, + AssignmentOperationType.ASSIGN, + AssignmentOperationType.ASSIGN_OR, + AssignmentOperationType.ASSIGN_CARET, + AssignmentOperationType.ASSIGN_AND, + AssignmentOperationType.ASSIGN_LEFT_SHIFT, + AssignmentOperationType.ASSIGN_RIGHT_SHIFT, + AssignmentOperationType.ASSIGN_MULTIPLICATION, + AssignmentOperationType.ASSIGN_DIVISION, + AssignmentOperationType.ASSIGN_MODULO +] + +class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "ASOR" + HELP = "Assignment Operator Replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: + if ir.expression.type == AssignmentOperationType.ASSIGN: + continue + alternative_ops = assignment_operators[:] + try: + alternative_ops.remove(ir.expression.type) + except: + continue + for op in assignment_operators: + if op != ir.expression: + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py new file mode 100644 index 000000000..7878a20ea --- /dev/null +++ b/slither/tools/mutator/mutators/BOR.py @@ -0,0 +1,35 @@ +from typing import Dict +from slither.slithir.operations import Binary, BinaryType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass + +bitwise_operators = [ + BinaryType.AND, + BinaryType.OR +] + +class BOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "BOR" + HELP = "Bitwise Operator Replacement" + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, Binary) and ir.type in bitwise_operators: + alternative_ops = bitwise_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index 382a08aee..b00b13000 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -15,17 +15,9 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - contract = self.contract - - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - for function in contract.functions_and_modifiers_declared: - + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -37,11 +29,11 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index d76369d4d..1cc0fdb6d 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -2,6 +2,7 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" @@ -10,32 +11,31 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - # Retrieve the file - in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = self.contract.compilation_unit.core.source_code[in_file] - for function in self.contract.functions_declared + list(self.contract.modifiers_declared): + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IF: - # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + # print(node.expression) + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - +# limitations - won't work if it is tenary operation \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 15b2a20c3..13a4e6d1a 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,9 +1,8 @@ from typing import Dict - from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.generic_patching import remove_assignement +from slither.formatters.utils.patches import create_patch class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" @@ -12,24 +11,52 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - contract = self.contract # Create fault for state variables declaration - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: continue if not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) - - for function in contract.functions_declared + list(contract.modifiers_declared): + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) + + for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and not isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 5e7c0a6e1..e705ff94a 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,9 +1,9 @@ -from typing import Dict, Tuple +from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.tools.mutator.utils.generic_patching import remove_assignement +from slither.formatters.utils.patches import create_patch class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" @@ -12,24 +12,51 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - contract = self.contract - + # Create fault for state variables declaration - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: continue if isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + # Get the string + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] - for function in contract.functions_declared + list(contract.modifiers_declared): + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) + + for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): - remove_assignement(variable, contract, result) + start = variable.source_mapping.start + stop = variable.expression.source_mapping.start + old_str = self.in_file_str[start:stop] + + new_str = old_str[: old_str.find("=")] + line_no = [0] + create_patch( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no + ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py new file mode 100644 index 000000000..1e2067425 --- /dev/null +++ b/slither/tools/mutator/mutators/MWA.py @@ -0,0 +1,34 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + +class MWA(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "MIA" + HELP = '"while" construct around statement' + FAULTCLASS = FaultClass.Checking + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type == NodeType.IFLOOP: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + + + +# limitations - won't work if it is tenary operation + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 9a8942e19..6a2ef6426 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,5 +1,4 @@ from typing import Dict -from collections import defaultdict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass @@ -22,19 +21,11 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - # result["patches"] = defaultdict(list) - contract = self.contract - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: - # Retrieve the file - in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = self.contract.compilation_unit.core.source_code[in_file] - if isinstance(ir, Binary) and ir.type in relational_operators: alternative_ops = relational_operators[:] alternative_ops.remove(ir.type) @@ -43,11 +34,12 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + +# failing in case of condition1 || condition2 \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index ac35d9540..6797c0c35 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -3,6 +3,7 @@ from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass import re +from slither.core.variables.variable import Variable solidity_rules = [ "abi\.encode\( ==> abi.encodePacked(", @@ -38,7 +39,8 @@ solidity_rules = [ "(u?)int32 ==> \\1int16", "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", - "(u?)int256 ==> \\1int128" + "(u?)int256 ==> \\1int128" + "while ==> if", ] @@ -51,40 +53,36 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - contract = self.contract - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] + variable: Variable - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type != NodeType.ENTRYPOINT: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - for variable in contract.state_variables_declared: + for variable in self.contract.state_variables_declared: node = variable.node_initialization if node: start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, in_file, start, stop, old_str, new_str, line_no[0]) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOI.py index 6d5862e76..60e8c22c3 100644 --- a/slither/tools/mutator/mutators/UOI.py +++ b/slither/tools/mutator/mutators/UOI.py @@ -1,23 +1,19 @@ from typing import Dict -import re -from slither.core.expressions.unary_operation import UnaryOperationType +from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation +from slither.core.expressions.expression import Expression from slither.slithir.variables import Constant from slither.core.variables.local_variable import LocalVariable -from slither.core.expressions.expression import Expression from slither.formatters.utils.patches import create_patch from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass -from slither.core.cfg.node import NodeType - unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, UnaryOperationType.MINUSMINUS_PRE, UnaryOperationType.PLUSPLUS_POST, UnaryOperationType.MINUSMINUS_POST, - UnaryOperationType.MINUS_PRE, + UnaryOperationType.MINUS_PRE ] - class UOI(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOI" HELP = "Unary operator insertion" @@ -25,32 +21,35 @@ class UOI(AbstractMutator): # pylint: disable=too-few-public-methods FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: - result: Dict = {} - contract = self.contract - - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - for function in contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if (node.type == NodeType.EXPRESSION): + try: + ir_expression = node.expression + except Exception as e: + continue + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: for op in unary_operators: - if str(op) in str(node.expression): - for i in node.variables_written: - print(i) - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = in_file_str[start:stop] - # print(old_str) - # Replace the expression with true - # new_str = old_str.replace(str(operand), f"{str(op)}{operand}") - # new_str = re.sub(r'(\w+)\+\+', r'++\1', text) - # print(new_str) - # create_patch(result, in_file, start, stop, old_str, new_str) - print(result) + if not node.expression.is_prefix: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(variable_read) + str(op) + if new_str != old_str: + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(op) + str(variable_read) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + else: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(op) + str(variable_read) + if new_str != old_str: + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(variable_read) + str(op) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index e58b705a1..ead78ac97 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -38,12 +38,15 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- INVALID_MUTANTS_COUNT = 0 def __init__( - self, compilation_unit: SlitherCompilationUnit, rate: int = 10, seed: Optional[int] = None + self, compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, contract_name: str, rate: int = 10, seed: Optional[int] = None ): self.compilation_unit = compilation_unit self.slither = compilation_unit.core self.seed = seed self.rate = rate + self.test_command = testing_command + self.test_directory = testing_directory + self.timeout = timeout if not self.NAME: raise IncorrectMutatorInitialization( @@ -69,18 +72,23 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) + + # identify the main contract, ignore the imports + for contract in self.slither.contracts: + if contract_name == str(contract.name): # limitation: what if the contract name is not same as file name + # contract + self.contract = contract + # Retrieve the file + self.in_file = self.contract.source_mapping.filename.absolute + # Retrieve the source code + self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] @abc.abstractmethod def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" return {} - def mutate(self, testing_command: str, testing_directory: str, contract_name: str) -> Tuple[int, int]: - # identify the main contract, ignore the imports - for contract in self.slither.contracts: - if contract_name == str(contract.name): - self.contract = contract - + def mutate(self) -> Tuple[int, int]: # call _mutate function from different mutators (all_patches) = self._mutate() @@ -98,9 +106,8 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # logger.error(f"Impossible to generate patch; patches collisions: {patches}") # continue for patch in patches: - # print(patch) # test the patch - flag = test_patch(file, patch, testing_command, self.VALID_MUTANTS_COUNT, self.NAME) + flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 4b0c3b1ad..3f4e59043 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,9 +1,13 @@ # pylint: disable=unused-import -# from slither.tools.mutator.mutators.MVIV import MVIV -# from slither.tools.mutator.mutators.MVIE import MVIE -# from slither.tools.mutator.mutators.MIA import MIA +from slither.tools.mutator.mutators.MVIV import MVIV +from slither.tools.mutator.mutators.MVIE import MVIE +from slither.tools.mutator.mutators.MIA import MIA from slither.tools.mutator.mutators.ROR import ROR -# from slither.tools.mutator.mutators.LOR import LOR -# from slither.tools.mutator.mutators.UOI import UOI -# from slither.tools.mutator.mutators.SBR import SBR +from slither.tools.mutator.mutators.LOR import LOR +from slither.tools.mutator.mutators.UOI import UOI +from slither.tools.mutator.mutators.SBR import SBR +from slither.tools.mutator.mutators.AOR import AOR +from slither.tools.mutator.mutators.BOR import BOR +from slither.tools.mutator.mutators.ASOR import ASOR +from slither.tools.mutator.mutators.MWA import MWA diff --git a/slither/tools/mutator/utils/generic_patching.py b/slither/tools/mutator/utils/generic_patching.py deleted file mode 100644 index 6c0ba6511..000000000 --- a/slither/tools/mutator/utils/generic_patching.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Dict -import os - -from slither.core.declarations import Contract -from slither.core.variables.variable import Variable -from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.utils.testing_generated_mutant import compile_generated_mutant, run_test_suite -from slither.tools.mutator.utils.replace_conditions import replace_string_in_source_file -from slither.tools.mutator.utils.file_handling import create_mutant_file - -def remove_assignement(variable: Variable, contract: Contract, result: Dict) -> bool: - """ - Remove the variable's initial assignement - - :param variable: - :param contract: - :param result: - :return: - """ - # Retrieve the file - in_file = contract.source_mapping.filename.absolute - # Retrieve the source code - in_file_str = contract.compilation_unit.core.source_code[in_file] - - # Get the string - start = variable.source_mapping.start - stop = variable.expression.source_mapping.start - old_str = in_file_str[start:stop] - - new_str = old_str[: old_str.find("=")] - line_no = [0] - create_patch( - result, - in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no - ) \ No newline at end of file diff --git a/slither/tools/mutator/utils/replace_conditions.py b/slither/tools/mutator/utils/replace_conditions.py deleted file mode 100644 index 4e3f91454..000000000 --- a/slither/tools/mutator/utils/replace_conditions.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import re - -logger = logging.getLogger("Slither-Mutate") - -# function to replace the string -def replace_string_in_source_file(file_path: str, old_string: str, new_string: str) -> None: - try: - # Read the content of the Solidity file - with open(file_path, 'r') as file: - content = file.read() - - # Perform the string replacement - modified_content = content.replace(old_string, new_string) - - # Write the modified content back to the file - with open(file_path, 'w') as file: - file.write(modified_content) - - logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.") - except Exception as e: - logger.error(f"Error replacing string: {e}") - -# function to replace the string in a specific line -def replace_string_in_source_file_specific_line(file_path: str, old_string: str, new_string: str, line_number : int) -> None: - try: - # Read the content of the Solidity file - with open(file_path, 'r') as file: - lines = file.readlines() - - if 1 <= line_number <= len(lines): - # remove the spaces in the string - line = lines[line_number - 1].replace(" ", "") - old_string = old_string.replace(" ", "") - - # Replace the old string with the new string on the specified line - lines[line_number - 1] = line.replace(old_string.strip(), new_string) - - # Write the modified content back to the file - with open(file_path, 'w') as file: - file.writelines(lines) - - logger.info(f"String '{old_string}' replaced with '{new_string}' in '{file_path}'.' at '{line_number}") - else: - logger.error(f'Error: Line number {line_number} is out of range') - - except Exception as e: - logger.erro(f'Error: {e}') \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 7ab406519..61a688f7c 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -38,14 +38,12 @@ def run_test_suite(cmd: str, dir: str) -> bool: logger.error(f"An unexpected error occurred: {e}") return False -def run_test_cmd(cmd: str, dir: str) -> bool: +def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: start = time.time() - # starting new process P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) - try: - # checking whether the process is completed or not for 30 seconds + # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: time.sleep(0.05) finally: @@ -58,21 +56,20 @@ def run_test_cmd(cmd: str, dir: str) -> bool: time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode - + # if r is 0 then it is valid mutant because tests didn't fail return True if r == 0 else False -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str) -> bool: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int) -> bool: with open(file, 'r') as filepath: content = filepath.read() # Perform the replacement based on the index values replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] - # Write the modified content back to the file with open(file, 'w') as filepath: filepath.write(replaced_content) if(compile_generated_mutant(file)): - if(run_test_cmd(command, file)): + if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) return True From df42cb66d18af3132c326b26759ce670144ad7e2 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sun, 14 Jan 2024 19:19:58 -0500 Subject: [PATCH 18/41] Updated mutators --- slither/tools/mutator/__main__.py | 48 +++++++---- slither/tools/mutator/mutators/AOR.py | 10 ++- slither/tools/mutator/mutators/ASOR.py | 6 +- slither/tools/mutator/mutators/BOR.py | 8 +- slither/tools/mutator/mutators/FHR.py | 36 ++++++++ slither/tools/mutator/mutators/LIR.py | 82 ++++++++++++++++++ slither/tools/mutator/mutators/LOR.py | 7 +- slither/tools/mutator/mutators/MIA.py | 18 ++-- slither/tools/mutator/mutators/MVIE.py | 11 ++- slither/tools/mutator/mutators/MVIV.py | 13 ++- slither/tools/mutator/mutators/MWA.py | 13 +-- slither/tools/mutator/mutators/RCR.py | 36 ++++++++ slither/tools/mutator/mutators/ROR.py | 34 ++++---- slither/tools/mutator/mutators/SBR.py | 9 +- .../tools/mutator/mutators/{UOI.py => UOR.py} | 18 ++-- .../mutator/mutators/abstract_mutator.py | 86 +++++++++++-------- .../tools/mutator/mutators/all_mutators.py | 26 +++--- slither/tools/mutator/utils/command_line.py | 11 ++- slither/tools/mutator/utils/file_handling.py | 4 +- .../mutator/utils/testing_generated_mutant.py | 58 +++++++------ 20 files changed, 351 insertions(+), 183 deletions(-) create mode 100644 slither/tools/mutator/mutators/FHR.py create mode 100644 slither/tools/mutator/mutators/LIR.py create mode 100644 slither/tools/mutator/mutators/RCR.py rename slither/tools/mutator/mutators/{UOI.py => UOR.py} (80%) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index aba427355..c41a82e35 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,12 +2,10 @@ import argparse import inspect import logging import sys -from typing import Type, List, Any, Dict, Tuple import os import shutil - +from typing import Type, List, Any from crytic_compile import cryticparser - from slither import Slither from slither.tools.mutator.mutators import all_mutators from .mutators.abstract_mutator import AbstractMutator @@ -62,13 +60,27 @@ def parse_args() -> argparse.Namespace: # time out argument parser.add_argument( "--timeout", - help="Set timeout for test command (by deafult 30 seconds)" + help="Set timeout for test command (by default 30 seconds)" ) # output directory argument parser.add_argument( "--output-dir", - help="Output Directory (by default it is 'mutation_campaign')" + help="Output Directory (by default 'mutation_campaign')" + ) + + # to print just all the mutants + parser.add_argument( + "--verbose", + help="output all mutants generated", + action="store_true", + default=False, + ) + + # select list of mutators to run + parser.add_argument( + "--mutators-to-run", + help="mutant generators to run", ) # Initiate all the crytic config cli options @@ -80,13 +92,14 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() - -def _get_mutators() -> List[Type[AbstractMutator]]: +def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator)] + if not mutators_list is None: + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] + else: + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] return detectors - class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any @@ -105,13 +118,15 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def main() -> None: args = parse_args() - # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir paths_to_ignore: str | None = args.ignore_dirs output_dir: str | None = args.output_dir timeout: int | None = args.timeout + solc_remappings: str | None = args.solc_remaps + verbose: bool = args.verbose + mutators_to_run: List[str] | None = args.mutators_to_run print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -137,6 +152,7 @@ def main() -> None: for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] + # TODO: user provides contract name # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -149,11 +165,13 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - for M in _get_mutators(): - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name) - count_valid, count_invalid = m.mutate() - v_count += count_valid - total_count += count_valid + count_invalid + for M in _get_mutators(mutators_to_run): + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) + # check whether the contract instance exists or not + if m.get_exist_flag(): + count_valid, count_invalid = m.mutate() + v_count += count_valid + total_count += count_valid + count_invalid except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index f248d3c77..39e3a6a20 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,7 +1,8 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.core.expressions.unary_operation import UnaryOperation arithmetic_operators = [ BinaryType.ADDITION, @@ -14,7 +15,6 @@ arithmetic_operators = [ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -22,8 +22,14 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: + try: + ir_expression = node.expression + except: + continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: + if isinstance(ir_expression, UnaryOperation): + continue alternative_ops = arithmetic_operators[:] alternative_ops.remove(ir.type) for op in alternative_ops: diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 8c70756a2..04ad8f8b0 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,7 +1,6 @@ from typing import Dict -from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation assignment_operators = [ @@ -21,7 +20,6 @@ assignment_operators = [ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -38,7 +36,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods alternative_ops.remove(ir.expression.type) except: continue - for op in assignment_operators: + for op in alternative_ops: if op != ir.expression: start = node.source_mapping.start stop = start + node.source_mapping.length diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 7878a20ea..de9ad287e 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,17 +1,19 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature bitwise_operators = [ BinaryType.AND, - BinaryType.OR + BinaryType.OR, + BinaryType.LEFT_SHIFT, + BinaryType.RIGHT_SHIFT, + BinaryType.CARET ] class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py new file mode 100644 index 000000000..5709346b1 --- /dev/null +++ b/slither/tools/mutator/mutators/FHR.py @@ -0,0 +1,36 @@ +from typing import Dict +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +import re + +# INFO: low severity + +function_header_replacements = [ + "pure ==> view", + "view ==> pure", + "(\s)(external|public|internal) ==> \\1private", + "(\s)(external|public) ==> \\1internal" +] + +class FHR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "FHR" + HELP = 'Function Header Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + # function_header = function.source_mapping.content.split('{')[0] + start = function.source_mapping.start + stop = start + function.source_mapping.content.find('{') + old_str = self.in_file_str[start:stop] + line_no = function.source_mapping.lines + for value in function_header_replacements: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py new file mode 100644 index 000000000..99228913a --- /dev/null +++ b/slither/tools/mutator/mutators/LIR.py @@ -0,0 +1,82 @@ +from typing import Dict +from slither.core.expressions import Literal +from slither.core.variables.variable import Variable +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.formatters.utils.patches import create_patch +from slither.core.solidity_types import ElementaryType + +literal_replacements = [] + +class LIR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "LIR" + HELP = "Literal Interger Replacement" + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + variable: Variable + + # Create fault for state variables declaration + for variable in self.contract.state_variables_declared: + if variable.initialized: + # Cannot remove the initialization of constant variables + if variable.is_constant: + continue + + if isinstance(variable.expression, Literal): + if isinstance(variable.type, ElementaryType): + literal_replacements.append(variable.type.min) # append data type min value + literal_replacements.append(variable.type.max) # append data type max value + if str(variable.type).startswith("uint"): + literal_replacements.append('1') + elif str(variable.type).startswith("uint"): + literal_replacements.append('-1') + # Get the string + start = variable.source_mapping.start + stop = start + variable.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = variable.node_initialization.source_mapping.lines + # line_no = [0] + for value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != value: + new_str = f"{old_str.split('=')[0]}= {value}" + create_patch( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) + + for function in self.contract.functions_and_modifiers_declared: + for variable in function.local_variables: + if variable.initialized and isinstance(variable.expression, Literal): + if isinstance(variable.type, ElementaryType): + literal_replacements.append(variable.type.min) + literal_replacements.append(variable.type.max) + if str(variable.type).startswith("uint"): + literal_replacements.append('1') + elif str(variable.type).startswith("uint"): + literal_replacements.append('-1') + start = variable.source_mapping.start + stop = start + variable.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = variable.source_mapping.lines + for new_value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != new_value: + new_str = f"{old_str.split('=')[0]}= {new_value}" + create_patch( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) + + return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index b00b13000..fa66c7bca 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature logical_operators = [ BinaryType.OROR, @@ -10,8 +10,7 @@ logical_operators = [ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" - HELP = "Logical operator replacement" - FAULTCLASS = FaultClass.Checking + HELP = "Logical Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -25,14 +24,12 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods alternative_ops.remove(ir.type) for op in alternative_ops: - # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines # Replace the expression with true - # new_str = f"{ir.variable_left} {op.value} {ir.variable_right}" new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 1cc0fdb6d..f5bc52b16 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -17,8 +16,8 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods for node in function.nodes: if node.type == NodeType.IF: # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length + start = node.expression.source_mapping.start + stop = start + node.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines @@ -27,15 +26,10 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods new_str = value create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - # print(node.expression) if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result - - -# limitations - won't work if it is tenary operation - - \ No newline at end of file + print(node.expression) + + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 13a4e6d1a..3c334e9e6 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.formatters.utils.patches import create_patch class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" - FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -28,7 +27,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.node_initialization.source_mapping.lines create_patch( result, self.in_file, @@ -36,7 +35,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) for function in self.contract.functions_and_modifiers_declared: @@ -48,7 +47,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.source_mapping.lines create_patch( result, self.in_file, @@ -56,7 +55,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index e705ff94a..7f75b1dcf 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -2,13 +2,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.formatters.utils.patches import create_patch class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" - FAULTCLASS = FaultClass.Assignement FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,9 +26,8 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.node_initialization.source_mapping.lines create_patch( result, self.in_file, @@ -37,7 +35,7 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) for function in self.contract.functions_and_modifiers_declared: @@ -46,9 +44,8 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] - line_no = [0] + line_no = variable.source_mapping.lines create_patch( result, self.in_file, @@ -56,7 +53,7 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no + line_no[0] ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 1e2067425..c6bacf485 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MWA(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "MIA" + NAME = "MWA" HELP = '"while" construct around statement' - FAULTCLASS = FaultClass.Checking FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -25,10 +24,4 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result - - - -# limitations - won't work if it is tenary operation - - \ No newline at end of file + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/RCR.py b/slither/tools/mutator/mutators/RCR.py new file mode 100644 index 000000000..96aebc68f --- /dev/null +++ b/slither/tools/mutator/mutators/RCR.py @@ -0,0 +1,36 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature + + +class RCR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "RCR" + HELP = 'Revert and Comment Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + if old_str != 'revert()': + new_str = 'revert()' + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + new_str = "//" + old_str + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 6a2ef6426..e1c78c1eb 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,8 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass - +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature relational_operators = [ BinaryType.LESS, @@ -13,11 +12,9 @@ relational_operators = [ BinaryType.NOT_EQUAL, ] - class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" - HELP = "Relational operator replacement" - FAULTCLASS = FaultClass.Checking + HELP = "Relational Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,19 +24,18 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: - alternative_ops = relational_operators[:] - alternative_ops.remove(ir.type) - - for op in alternative_ops: - # Get the string - start = node.source_mapping.start - stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] - line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if str(ir.variable_left.type) != 'address' and str(ir.variable_right) != 'address': + alternative_ops = relational_operators[:] + alternative_ops.remove(ir.type) + for op in alternative_ops: + # Get the string + start = ir.expression.source_mapping.start + stop = start + ir.expression.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + + create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result -# failing in case of condition1 || condition2 \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 6797c0c35..91c05b884 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re from slither.core.variables.variable import Variable @@ -39,15 +39,14 @@ solidity_rules = [ "(u?)int32 ==> \\1int16", "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", - "(u?)int256 ==> \\1int128" - "while ==> if", + "(u?)int256 ==> \\1int128", + "while ==> if", ] class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" - HELP = 'Solidity Based Replacements' - FAULTCLASS = FaultClass.Checking + HELP = 'Solidity Based Replacement' FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/UOI.py b/slither/tools/mutator/mutators/UOR.py similarity index 80% rename from slither/tools/mutator/mutators/UOI.py rename to slither/tools/mutator/mutators/UOR.py index 60e8c22c3..671ee676d 100644 --- a/slither/tools/mutator/mutators/UOI.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,10 +1,7 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation -from slither.core.expressions.expression import Expression -from slither.slithir.variables import Constant -from slither.core.variables.local_variable import LocalVariable from slither.formatters.utils.patches import create_patch -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature, FaultClass +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, @@ -14,10 +11,9 @@ unary_operators = [ UnaryOperationType.MINUS_PRE ] -class UOI(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "UOI" - HELP = "Unary operator insertion" - FAULTCLASS = FaultClass.Checking +class UOR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "UOR" + HELP = "Unary Operator Replacement" FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -27,7 +23,7 @@ class UOI(AbstractMutator): # pylint: disable=too-few-public-methods for node in function.nodes: try: ir_expression = node.expression - except Exception as e: + except: continue start = node.source_mapping.start stop = start + node.source_mapping.length @@ -39,7 +35,7 @@ class UOI(AbstractMutator): # pylint: disable=too-few-public-methods if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) - if new_str != old_str: + if new_str != old_str and str(op) != '-': create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(op) + str(variable_read) create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) @@ -47,7 +43,7 @@ class UOI(AbstractMutator): # pylint: disable=too-few-public-methods if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) - if new_str != old_str: + if new_str != old_str and str(op) != '-': create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ead78ac97..5223e2d81 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,45 +1,48 @@ import abc import logging from enum import Enum -from typing import Optional, Dict, Tuple, List - +from typing import Optional, Dict, Tuple from slither.core.compilation_unit import SlitherCompilationUnit -from slither.tools.doctor.utils import snip_section +# from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch -logger = logging.getLogger("Slither-Mutate") +from slither.utils.colors import yellow +logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): pass - -class FaultClass(Enum): - Assignement = 0 - Checking = 1 - Interface = 2 - Algorithm = 3 - Undefined = 100 - - class FaultNature(Enum): Missing = 0 Wrong = 1 Extraneous = 2 Undefined = 100 - + # not executed - can be detected by replacing with revert + # has no effect - can be detected by removing a line / comment + # can have valid mutant + # can't have valid mutant + class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" - FAULTCLASS = FaultClass.Undefined FAULTNATURE = FaultNature.Undefined VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 def __init__( - self, compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, contract_name: str, rate: int = 10, seed: Optional[int] = None - ): + self, compilation_unit: SlitherCompilationUnit, + timeout: int, + testing_command: str, + testing_directory: str, + contract_name: str, + solc_remappings: str | None, + verbose: bool, + output_folder: str, + rate: int = 10, + seed: Optional[int] = None + ) -> None: self.compilation_unit = compilation_unit self.slither = compilation_unit.core self.seed = seed @@ -47,6 +50,10 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- self.test_command = testing_command self.test_directory = testing_directory self.timeout = timeout + self.contract_exist = False + self.solc_remappings = solc_remappings + self.verbose = verbose + self.output_folder = output_folder if not self.NAME: raise IncorrectMutatorInitialization( @@ -58,11 +65,6 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- f"HELP is not initialized {self.__class__.__name__}" ) - if self.FAULTCLASS == FaultClass.Undefined: - raise IncorrectMutatorInitialization( - f"FAULTCLASS is not initialized {self.__class__.__name__}" - ) - if self.FAULTNATURE == FaultNature.Undefined: raise IncorrectMutatorInitialization( f"FAULTNATURE is not initialized {self.__class__.__name__}" @@ -75,14 +77,25 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # identify the main contract, ignore the imports for contract in self.slither.contracts: - if contract_name == str(contract.name): # limitation: what if the contract name is not same as file name + # !limitation: what if the contract name is not same as file name + # !limitation: multi contract + if contract_name.lower() == str(contract.name).lower(): # contract self.contract = contract # Retrieve the file self.in_file = self.contract.source_mapping.filename.absolute # Retrieve the source code self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] - + # flag contract existence + self.contract_exist = True + + if not self.contract_exist: + self.contract_exist = False + logger.error(f"Contract name is not matching with the File name ({contract_name}). Please refer 'https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names')") + + def get_exist_flag(self) -> bool: + return self.contract_exist + @abc.abstractmethod def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """TODO Documentation""" @@ -95,31 +108,28 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") return (0,0) - + for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") - patched_txt = original_txt - offset = 0 patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - # if not all(patches[i]["end"] <= patches[i + 1]["end"] for i in range(len(patches) - 1)): - # logger.error(f"Impossible to generate patch; patches collisions: {patches}") - # continue + print(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch - flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout) + flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 continue self.VALID_MUTANTS_COUNT += 1 - # patched_txt, offset = apply_patch(patched_txt, patch, offset) - # diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) - # if not diff: - # logger.info(f"Impossible to generate patch; empty {patches}") - - # print the differences - # print(diff) + patched_txt,_ = apply_patch(original_txt, patch, 0) + diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + if not diff: + logger.info(f"Impossible to generate patch; empty {patches}") + + # add valid mutant patches to a output file + with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: + patches_file.write(diff + '\n') return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 3f4e59043..9aa81a0fc 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,13 +1,17 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV -from slither.tools.mutator.mutators.MVIE import MVIE -from slither.tools.mutator.mutators.MIA import MIA -from slither.tools.mutator.mutators.ROR import ROR -from slither.tools.mutator.mutators.LOR import LOR -from slither.tools.mutator.mutators.UOI import UOI -from slither.tools.mutator.mutators.SBR import SBR -from slither.tools.mutator.mutators.AOR import AOR -from slither.tools.mutator.mutators.BOR import BOR -from slither.tools.mutator.mutators.ASOR import ASOR -from slither.tools.mutator.mutators.MWA import MWA +from slither.tools.mutator.mutators.MVIV import MVIV # severity low +from slither.tools.mutator.mutators.MVIE import MVIE # severity low +from slither.tools.mutator.mutators.LOR import LOR # severity medium +from slither.tools.mutator.mutators.UOR import UOR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.AOR import AOR # severity medium +from slither.tools.mutator.mutators.BOR import BOR # severity medium +from slither.tools.mutator.mutators.ASOR import ASOR # severity medium +from slither.tools.mutator.mutators.MWA import MWA # severity medium +from slither.tools.mutator.mutators.LIR import LIR # severity medium +from slither.tools.mutator.mutators.FHR import FHR # severity medium +from slither.tools.mutator.mutators.MIA import MIA # severity medium +from slither.tools.mutator.mutators.ROR import ROR # severity medium +from slither.tools.mutator.mutators.RCR import RCR # severity high + diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 80d610a69..042b4fff7 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -7,15 +7,14 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: for detector in mutators_classes: argument = detector.NAME help_info = detector.HELP - fault_class = detector.FAULTCLASS.name fault_nature = detector.FAULTNATURE.name - mutators_list.append((argument, help_info, fault_class, fault_nature)) - table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Class", "Fault Nature"]) + mutators_list.append((argument, help_info, fault_nature)) + table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Nature"]) # Sort by class, nature, name - mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[3], element[0])) + mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[0])) idx = 1 - for (argument, help_info, fault_class, fault_nature) in mutators_list: - table.add_row([str(idx), argument, help_info, fault_class, fault_nature]) + for (argument, help_info, fault_nature) in mutators_list: + table.add_row([str(idx), argument, help_info, fault_nature]) idx = idx + 1 print(table) diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 70f7deb11..d61bba58a 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -105,5 +105,5 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str sol_file_list.append(i) return sol_file_list -# to_do: create a function to delete the commands from the sol file -# def remove_comments(self) -> None: \ No newline at end of file + +# TODO: create a function to delete the commands from the sol file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 61a688f7c..6c6e80a55 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -7,38 +7,44 @@ import signal from typing import List, Dict from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red + logger = logging.getLogger("Slither-Mutate") -timeout = 30 # we can get it as parameter # function to compile the generated mutant -def compile_generated_mutant(file_path: str) -> bool: +def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: - crytic_compile.CryticCompile(file_path) + crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True - except Exception as e: # pylint: disable=broad-except - print(True) + except: # pylint: disable=broad-except # logger.error("Error Crytic Compile") return False # function to run the tests -def run_test_suite(cmd: str, dir: str) -> bool: - try: - # Change to the foundry folder - # os.chdir(dir) +# def run_test_suite(cmd: str, dir: str) -> bool: +# try: +# # Change to the foundry folder +# # os.chdir(dir) - result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # result = subprocess.run(cmd.split(' '), check=True) - if not result.stderr: - return True - except subprocess.CalledProcessError as e: - logger.error(f"Error executing '{cmd}': {e}") +# result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +# # result = subprocess.run(cmd.split(' '), check=True) +# if not result.stderr: +# return True +# except subprocess.CalledProcessError as e: +# logger.error(f"Error executing '{cmd}': {e}") - return False - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") - return False +# return False +# except Exception as e: +# logger.error(f"An unexpected error occurred: {e}") +# return False def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + # add --fail-fast for foundry tests, to exit after first failure + if "forge test" in cmd and not "--fail-fast" in cmd : + cmd += " --fail-fast" + # add --bail for hardhat and truffle tests, to exit after first failure + elif not "--bail" in cmd: + cmd += " --bail" + start = time.time() # starting new process P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) @@ -48,19 +54,18 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: time.sleep(0.05) finally: if P.poll() is None: - print() - print("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") # sends a SIGTERM signal to process group - bascially killing the process os.killpg(os.getpgid(P.pid), signal.SIGTERM) # Avoid any weird race conditions from grabbing the return code time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode - + # if r is 0 then it is valid mutant because tests didn't fail return True if r == 0 else False -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int) -> bool: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: with open(file, 'r') as filepath: content = filepath.read() # Perform the replacement based on the index values @@ -68,12 +73,13 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: # Write the modified content back to the file with open(file, 'w') as filepath: filepath.write(replaced_content) - if(compile_generated_mutant(file)): + if(compile_generated_mutant(file, mappings)): if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) - logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> VALID\n")) + logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) return True reset_file(file) - logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' in '{file}' ---> INVALID\n")) + if verbose: + logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) return False \ No newline at end of file From 9db71678179c9b76546f91d0406cf52c40d970bc Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 16 Jan 2024 12:00:08 -0500 Subject: [PATCH 19/41] Updated run_test_cmd --- slither/tools/mutator/__main__.py | 1 + .../mutator/mutators/abstract_mutator.py | 2 +- .../mutator/utils/testing_generated_mutant.py | 23 ++----------------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index c41a82e35..8cbb103ad 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -165,6 +165,7 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: + # TODO for M in _get_mutators(mutators_to_run): m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) # check whether the contract instance exists or not diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 5223e2d81..bd40ad6ac 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -97,7 +97,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- return self.contract_exist @abc.abstractmethod - def _mutate(self, test_cmd: str, test_dir: str) -> Dict: + def _mutate(self) -> Dict: """TODO Documentation""" return {} diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 6c6e80a55..65cf78c27 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -16,33 +16,14 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool: crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True except: # pylint: disable=broad-except - # logger.error("Error Crytic Compile") return False - -# function to run the tests -# def run_test_suite(cmd: str, dir: str) -> bool: -# try: -# # Change to the foundry folder -# # os.chdir(dir) - -# result = subprocess.run(cmd.split(' '), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -# # result = subprocess.run(cmd.split(' '), check=True) -# if not result.stderr: -# return True -# except subprocess.CalledProcessError as e: -# logger.error(f"Error executing '{cmd}': {e}") - -# return False -# except Exception as e: -# logger.error(f"An unexpected error occurred: {e}") -# return False def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: # add --fail-fast for foundry tests, to exit after first failure - if "forge test" in cmd and not "--fail-fast" in cmd : + if "forge test" in cmd and not "--fail-fast" in cmd: cmd += " --fail-fast" # add --bail for hardhat and truffle tests, to exit after first failure - elif not "--bail" in cmd: + elif "hardhat test" in cmd or "truffle test" and not "--bail" in cmd: cmd += " --bail" start = time.time() From 6ad193abd99198bde804144d21fc29fcc89feb86 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 17 Jan 2024 11:54:06 -0500 Subject: [PATCH 20/41] updated create patch --- slither/tools/mutator/__main__.py | 30 +++++++++++++++-- slither/tools/mutator/mutators/AOR.py | 4 +-- slither/tools/mutator/mutators/ASOR.py | 4 +-- slither/tools/mutator/mutators/BOR.py | 4 +-- slither/tools/mutator/mutators/CR.py | 32 +++++++++++++++++++ slither/tools/mutator/mutators/FHR.py | 4 +-- slither/tools/mutator/mutators/LIR.py | 6 ++-- slither/tools/mutator/mutators/LOR.py | 4 +-- slither/tools/mutator/mutators/MIA.py | 6 ++-- slither/tools/mutator/mutators/MVIE.py | 6 ++-- slither/tools/mutator/mutators/MVIV.py | 7 ++-- slither/tools/mutator/mutators/MWA.py | 4 +-- slither/tools/mutator/mutators/ROR.py | 4 +-- .../tools/mutator/mutators/{RCR.py => RR.py} | 14 +++----- slither/tools/mutator/mutators/SBR.py | 6 ++-- slither/tools/mutator/mutators/UOR.py | 10 +++--- .../tools/mutator/mutators/all_mutators.py | 5 ++- slither/tools/mutator/utils/file_handling.py | 2 +- slither/tools/mutator/utils/patch.py | 22 +++++++++++++ .../mutator/utils/testing_generated_mutant.py | 2 +- 20 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 slither/tools/mutator/mutators/CR.py rename slither/tools/mutator/mutators/{RCR.py => RR.py} (66%) create mode 100644 slither/tools/mutator/utils/patch.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 8cbb103ad..66c0941a6 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -83,6 +83,20 @@ def parse_args() -> argparse.Namespace: help="mutant generators to run", ) + # list of contract names you want to mutate + parser.add_argument( + "--contract-names", + help="list of contract names you want to mutate", + ) + + # flag to run full mutation based revert mutator output + parser.add_argument( + "--quick", + help="to stop full mutation if revert mutator passes", + action="store_true", + default=False, + ) + # Initiate all the crytic config cli options cryticparser.init(parser) @@ -118,6 +132,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def main() -> None: args = parse_args() + # arguments test_command: str = args.test_cmd test_directory: str = args.test_dir @@ -127,6 +142,8 @@ def main() -> None: solc_remappings: str | None = args.solc_remaps verbose: bool = args.verbose mutators_to_run: List[str] | None = args.mutators_to_run + contract_names: List[str] | None = args.contract_names + quick_flag: bool = args.quick print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) @@ -150,6 +167,13 @@ def main() -> None: if timeout == None: timeout = 30 + # setting RR mutator as first mutator + mutators_list = _get_mutators(mutators_to_run) + for M in mutators_list: + if M.NAME == "RR": + mutators_list.remove(M) + mutators_list.insert(0, M) + for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] # TODO: user provides contract name @@ -165,14 +189,16 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - # TODO - for M in _get_mutators(mutators_to_run): + for M in mutators_list: m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) # check whether the contract instance exists or not if m.get_exist_flag(): count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid + if quick_flag: + if str(m.NAME) == 'RR' and v_count > 0: + break except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 39e3a6a20..61eed67b1 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperation @@ -40,5 +40,5 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 04ad8f8b0..e6fa41437 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,5 +1,5 @@ from typing import Dict -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation @@ -44,5 +44,5 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index de9ad287e..5ca46950f 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature bitwise_operators = [ @@ -33,5 +33,5 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py new file mode 100644 index 000000000..1935c43e6 --- /dev/null +++ b/slither/tools/mutator/mutators/CR.py @@ -0,0 +1,32 @@ +from typing import Dict +from slither.core.cfg.node import NodeType +from slither.tools.mutator.utils.patch import create_patch_with_line +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature + + +class CR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "CR" + HELP = 'Comment Replacement' + FAULTNATURE = FaultNature.Missing + + def _mutate(self) -> Dict: + result: Dict = {} + + for function in self.contract.functions_and_modifiers_declared: + for node in function.nodes: + if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + # Get the string + start = node.source_mapping.start + stop = start + node.source_mapping.length + old_str = self.in_file_str[start:stop] + line_no = node.source_mapping.lines + new_str = "//" + old_str + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + return result + + + + + + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 5709346b1..3758306f3 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,5 +1,5 @@ from typing import Dict -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re @@ -31,6 +31,6 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 99228913a..c9966f1be 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -2,7 +2,7 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.core.solidity_types import ElementaryType literal_replacements = [] @@ -41,7 +41,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods old_value = old_str[old_str.find("=")+1:].strip() if old_value != value: new_str = f"{old_str.split('=')[0]}= {value}" - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -69,7 +69,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods old_value = old_str[old_str.find("=")+1:].strip() if old_value != new_value: new_str = f"{old_str.split('=')[0]}= {new_value}" - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index fa66c7bca..e6903a01e 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature logical_operators = [ @@ -32,5 +32,5 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index f5bc52b16..0217324e0 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation @@ -24,11 +24,11 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) print(node.expression) diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 3c334e9e6..c81182c43 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -2,7 +2,7 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" @@ -28,7 +28,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -48,7 +48,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 7f75b1dcf..890c55b1c 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,9 +1,8 @@ from typing import Dict - from slither.core.expressions import Literal from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" @@ -28,7 +27,7 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, @@ -46,7 +45,7 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch( + create_patch_with_line( result, self.in_file, start, diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index c6bacf485..9bb6a1600 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation @@ -23,5 +23,5 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index e1c78c1eb..2a38ce323 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature relational_operators = [ @@ -36,6 +36,6 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/RCR.py b/slither/tools/mutator/mutators/RR.py similarity index 66% rename from slither/tools/mutator/mutators/RCR.py rename to slither/tools/mutator/mutators/RR.py index 96aebc68f..95cbf617a 100644 --- a/slither/tools/mutator/mutators/RCR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -1,12 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature -class RCR(AbstractMutator): # pylint: disable=too-few-public-methods - NAME = "RCR" - HELP = 'Revert and Comment Replacement' +class RR(AbstractMutator): # pylint: disable=too-few-public-methods + NAME = "RR" + HELP = 'Revert Replacement' FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: @@ -22,11 +22,7 @@ class RCR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines if old_str != 'revert()': new_str = 'revert()' - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - new_str = "//" + old_str - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 91c05b884..9e9afc1e5 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.cfg.node import NodeType -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re from slither.core.variables.variable import Variable @@ -67,7 +67,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) for variable in self.contract.state_variables_declared: node = variable.node_initialization @@ -81,7 +81,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) != None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index 671ee676d..aac4b02a6 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation -from slither.formatters.utils.patches import create_patch +from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature unary_operators = [ @@ -36,16 +36,16 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) if new_str != old_str and str(op) != '-': - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(op) + str(variable_read) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) else: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) if new_str != old_str and str(op) != '-': - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) - create_patch(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 9aa81a0fc..21925317d 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -12,6 +12,5 @@ from slither.tools.mutator.mutators.LIR import LIR # severity medium from slither.tools.mutator.mutators.FHR import FHR # severity medium from slither.tools.mutator.mutators.MIA import MIA # severity medium from slither.tools.mutator.mutators.ROR import ROR # severity medium -from slither.tools.mutator.mutators.RCR import RCR # severity high - - +from slither.tools.mutator.mutators.RR import RR # severity high +from slither.tools.mutator.mutators.CR import CR # severity high \ No newline at end of file diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index d61bba58a..01a142c8d 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -1,5 +1,5 @@ import os -from typing import Dict, Tuple, List +from typing import Dict, List import logging logger = logging.getLogger("Slither-Mutate") diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py new file mode 100644 index 000000000..39c77f673 --- /dev/null +++ b/slither/tools/mutator/utils/patch.py @@ -0,0 +1,22 @@ +from typing import Dict, Union +from collections import defaultdict + +# pylint: disable=too-many-arguments +def create_patch_with_line( + result: Dict, + file: str, + start: int, + end: int, + old_str: Union[str, bytes], + new_str: Union[str, bytes], + line_no: int +) -> None: + if isinstance(old_str, bytes): + old_str = old_str.decode("utf8") + if isinstance(new_str, bytes): + new_str = new_str.decode("utf8") + p = {"start": start, "end": end, "old_string": old_str, "new_string": new_str, "line_number": line_no} + if "patches" not in result: + result["patches"] = defaultdict(list) + if p not in result["patches"][file]: + result["patches"][file].append(p) \ No newline at end of file diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 65cf78c27..8525e2808 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -4,7 +4,7 @@ import os import logging import time import signal -from typing import List, Dict +from typing import Dict from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red From 96e8adc39b9eb0cc12c13d51b0c32437444665dd Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 18 Jan 2024 10:49:42 -0500 Subject: [PATCH 21/41] Added contract_names arg --- slither/tools/mutator/__main__.py | 16 +++++++---- slither/tools/mutator/mutators/FHR.py | 2 -- .../mutator/mutators/abstract_mutator.py | 28 ++++--------------- .../mutator/utils/testing_generated_mutant.py | 4 ++- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 66c0941a6..e576970f7 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -176,7 +176,6 @@ def main() -> None: for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] - # TODO: user provides contract name # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -189,10 +188,17 @@ def main() -> None: # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_name, solc_remappings, verbose, output_folder) - # check whether the contract instance exists or not - if m.get_exist_flag(): + contract_instance = '' + for contract in compilation_unit_of_main_file.contracts: + if contract_names != None and contract.name in contract_names: + contract_instance = contract + elif str(contract.name).lower() == contract_name.lower(): + contract_instance = contract + if contract_instance == '': + logger.error("Can't find the contract") + else: + for M in mutators_list: + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder) count_valid, count_invalid = m.mutate() v_count += count_valid total_count += count_valid + count_invalid diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 3758306f3..a5d7f5f7f 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -3,8 +3,6 @@ from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature import re -# INFO: low severity - function_header_replacements = [ "pure ==> view", "view ==> pure", diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index bd40ad6ac..edd584386 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -7,6 +7,7 @@ from slither.core.compilation_unit import SlitherCompilationUnit from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch from slither.utils.colors import yellow +from slither.core.declarations import Contract logger = logging.getLogger("Slither-Mutate") @@ -36,7 +37,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- timeout: int, testing_command: str, testing_directory: str, - contract_name: str, + contract_instance: Contract, solc_remappings: str | None, verbose: bool, output_folder: str, @@ -50,10 +51,12 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- self.test_command = testing_command self.test_directory = testing_directory self.timeout = timeout - self.contract_exist = False self.solc_remappings = solc_remappings self.verbose = verbose self.output_folder = output_folder + self.contract = contract_instance + self.in_file = self.contract.source_mapping.filename.absolute + self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] if not self.NAME: raise IncorrectMutatorInitialization( @@ -74,27 +77,6 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) - - # identify the main contract, ignore the imports - for contract in self.slither.contracts: - # !limitation: what if the contract name is not same as file name - # !limitation: multi contract - if contract_name.lower() == str(contract.name).lower(): - # contract - self.contract = contract - # Retrieve the file - self.in_file = self.contract.source_mapping.filename.absolute - # Retrieve the source code - self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] - # flag contract existence - self.contract_exist = True - - if not self.contract_exist: - self.contract_exist = False - logger.error(f"Contract name is not matching with the File name ({contract_name}). Please refer 'https://docs.soliditylang.org/en/latest/style-guide.html#contract-and-library-names')") - - def get_exist_flag(self) -> bool: - return self.contract_exist @abc.abstractmethod def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 8525e2808..2013858df 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -9,7 +9,7 @@ from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_ from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") - +# dont_mutate_line = {} # function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: @@ -58,6 +58,8 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + # if generator_name == 'RR': + # dont_mutate_line[patch['line_number']] = True return True reset_file(file) From 48b6f6fee2bd82ae968d48b00eac23f97f4d46f2 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sat, 20 Jan 2024 12:25:12 -0500 Subject: [PATCH 22/41] updated quick --- slither/tools/mutator/__main__.py | 22 +++++--- slither/tools/mutator/mutators/AOR.py | 8 +-- slither/tools/mutator/mutators/ASOR.py | 7 ++- slither/tools/mutator/mutators/BOR.py | 7 ++- slither/tools/mutator/mutators/CR.py | 5 +- slither/tools/mutator/mutators/FHR.py | 13 +++-- slither/tools/mutator/mutators/LIR.py | 55 ++++++++++--------- slither/tools/mutator/mutators/LOR.py | 8 +-- slither/tools/mutator/mutators/MIA.py | 20 +++---- slither/tools/mutator/mutators/MVIE.py | 40 +++++++------- slither/tools/mutator/mutators/MVIV.py | 38 +++++++------ slither/tools/mutator/mutators/MWA.py | 8 +-- slither/tools/mutator/mutators/ROR.py | 8 +-- slither/tools/mutator/mutators/RR.py | 7 ++- slither/tools/mutator/mutators/SBR.py | 28 +++++----- slither/tools/mutator/mutators/UOR.py | 35 ++++++------ .../mutator/mutators/abstract_mutator.py | 14 +++-- .../mutator/utils/testing_generated_mutant.py | 4 +- 18 files changed, 172 insertions(+), 155 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index e576970f7..80d025c97 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -169,10 +169,17 @@ def main() -> None: # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) - for M in mutators_list: + + CR_RR_list = [] + duplicate_list = mutators_list.copy() + for M in duplicate_list: if M.NAME == "RR": mutators_list.remove(M) - mutators_list.insert(0, M) + CR_RR_list.insert(0,M) + elif M.NAME == "CR": + mutators_list.remove(M) + CR_RR_list.insert(1,M) + mutators_list = CR_RR_list + mutators_list for filename in sol_file_list: contract_name = os.path.split(filename)[1].split('.sol')[0] @@ -185,6 +192,7 @@ def main() -> None: # count of valid mutants v_count = 0 + dont_mutate_lines = [] # mutation try: for compilation_unit_of_main_file in sl.compilation_units: @@ -198,13 +206,13 @@ def main() -> None: logger.error("Can't find the contract") else: for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder) - count_valid, count_invalid = m.mutate() + m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder, dont_mutate_lines) + (count_valid, count_invalid, lines_list) = m.mutate() v_count += count_valid total_count += count_valid + count_invalid - if quick_flag: - if str(m.NAME) == 'RR' and v_count > 0: - break + dont_mutate_lines = lines_list + if not quick_flag: + dont_mutate_lines = [] except Exception as e: logger.error(e) diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 61eed67b1..11593ee37 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -19,7 +19,6 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: @@ -38,7 +37,8 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index e6fa41437..29a6dc672 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -42,7 +42,8 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 5ca46950f..6a5552a5f 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -31,7 +31,8 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 1935c43e6..79a63a88f 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -20,8 +20,9 @@ class CR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - new_str = "//" + old_str - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + new_str = "//" + old_str + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index a5d7f5f7f..84bfebe22 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -24,11 +24,12 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + function.source_mapping.content.find('{') old_str = self.in_file_str[start:stop] line_no = function.source_mapping.lines - for value in function_header_replacements: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in function_header_replacements: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index c9966f1be..e8217e935 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -36,20 +36,20 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.node_initialization.source_mapping.lines - # line_no = [0] - for value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() - if old_value != value: - new_str = f"{old_str.split('=')[0]}= {value}" - create_patch_with_line( - result, - self.in_file, - start, - stop, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + for value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != value: + new_str = f"{old_str.split('=')[0]}= {value}" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -65,18 +65,19 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.source_mapping.lines - for new_value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() - if old_value != new_value: - new_str = f"{old_str.split('=')[0]}= {new_value}" - create_patch_with_line( - result, - self.in_file, - start, - stop, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + for new_value in literal_replacements: + old_value = old_str[old_str.find("=")+1:].strip() + if old_value != new_value: + new_str = f"{old_str.split('=')[0]}= {new_value}" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index e6903a01e..e14c85708 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -29,8 +29,8 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 0217324e0..421f94a5e 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -20,16 +20,14 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true and false + for value in ["true", "false"]: + new_str = value + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - # Replace the expression with true and false - for value in ["true", "false"]: - new_str = value - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - print(node.expression) - return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index c81182c43..4ae387d29 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -25,18 +25,18 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -45,17 +45,17 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods start = variable.source_mapping.start stop = variable.expression.source_mapping.start old_str = self.in_file_str[start:stop] - new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 890c55b1c..68bc2f2d8 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -27,15 +27,16 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.node_initialization.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: @@ -45,14 +46,15 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods old_str = self.in_file_str[start:stop] new_str = old_str[: old_str.find("=")] line_no = variable.source_mapping.lines - create_patch_with_line( - result, - self.in_file, - start, - stop + variable.expression.source_mapping.length, - old_str, - new_str, - line_no[0] - ) + if not line_no[0] in self.dont_mutate_line: + create_patch_with_line( + result, + self.in_file, + start, + stop + variable.expression.source_mapping.length, + old_str, + new_str, + line_no[0] + ) return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 9bb6a1600..4c1e91b32 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -20,8 +20,8 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - - if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + if not isinstance(node.expression, UnaryOperation): + new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 2a38ce323..da75a592a 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -33,9 +33,9 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + ir.expression.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - # Replace the expression with true - new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + # Replace the expression with true + new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 95cbf617a..3aab255ec 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -20,9 +20,10 @@ class RR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - if old_str != 'revert()': - new_str = 'revert()' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + if old_str != 'revert()': + new_str = 'revert()' + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 9e9afc1e5..f6adfff84 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -56,18 +56,19 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type != NodeType.ENTRYPOINT: + if node.type != NodeType.ENTRYPOINT and node.type != NodeType.ENDIF and node.type != NodeType.ENDLOOP: # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - for value in solidity_rules: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) for variable in self.contract.state_variables_declared: node = variable.node_initialization @@ -76,12 +77,13 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - for value in solidity_rules: - left_value = value.split(" ==> ")[0] - right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if not line_no[0] in self.dont_mutate_line: + for value in solidity_rules: + left_value = value.split(" ==> ")[0] + right_value = value.split(" ==> ")[1] + if re.search(re.compile(left_value), old_str) != None: + new_str = re.sub(re.compile(left_value), right_value, old_str) + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index aac4b02a6..fa00b52ae 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -29,23 +29,24 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines - if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: - for op in unary_operators: - if not node.expression.is_prefix: - if node.expression.type != op: - variable_read = node.variables_read[0] - new_str = str(variable_read) + str(op) - if new_str != old_str and str(op) != '-': + if not line_no[0] in self.dont_mutate_line: + if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: + for op in unary_operators: + if not node.expression.is_prefix: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(variable_read) + str(op) + if new_str != old_str and str(op) != '-': + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(op) + str(variable_read) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - new_str = str(op) + str(variable_read) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - else: - if node.expression.type != op: - variable_read = node.variables_read[0] - new_str = str(op) + str(variable_read) - if new_str != old_str and str(op) != '-': + else: + if node.expression.type != op: + variable_read = node.variables_read[0] + new_str = str(op) + str(variable_read) + if new_str != old_str and str(op) != '-': + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(variable_read) + str(op) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - new_str = str(variable_read) + str(op) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index edd584386..f11d69db1 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,7 +1,7 @@ import abc import logging from enum import Enum -from typing import Optional, Dict, Tuple +from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit # from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff @@ -41,6 +41,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- solc_remappings: str | None, verbose: bool, output_folder: str, + dont_mutate_line: List[int], rate: int = 10, seed: Optional[int] = None ) -> None: @@ -57,6 +58,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- self.contract = contract_instance self.in_file = self.contract.source_mapping.filename.absolute self.in_file_str = self.contract.compilation_unit.core.source_code[self.in_file] + self.dont_mutate_line = dont_mutate_line if not self.NAME: raise IncorrectMutatorInitialization( @@ -83,13 +85,12 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- """TODO Documentation""" return {} - def mutate(self) -> Tuple[int, int]: + def mutate(self) -> Tuple[int, int, List[int]]: # call _mutate function from different mutators (all_patches) = self._mutate() - if "patches" not in all_patches: logger.debug(f"No patches found by {self.NAME}") - return (0,0) + return (0,0,self.dont_mutate_line) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -99,6 +100,8 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + if (self.NAME == 'RR' or self.NAME == 'CR') and flag: + self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 @@ -112,8 +115,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT) + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 2013858df..8525e2808 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -9,7 +9,7 @@ from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_ from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") -# dont_mutate_line = {} + # function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: try: @@ -58,8 +58,6 @@ def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: if(run_test_cmd(command, file, timeout)): create_mutant_file(file, index, generator_name) logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) - # if generator_name == 'RR': - # dont_mutate_line[patch['line_number']] = True return True reset_file(file) From 799117d0868a0e2eaab0085b42eb416822942328 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Sun, 21 Jan 2024 12:08:13 -0500 Subject: [PATCH 23/41] Added README --- slither/tools/mutator/README.md | 31 +++++++++++++++++++ slither/tools/mutator/__main__.py | 12 ++++--- .../mutator/mutators/abstract_mutator.py | 2 +- slither/tools/mutator/utils/file_handling.py | 4 +-- 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 slither/tools/mutator/README.md diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md new file mode 100644 index 000000000..11b0b7eb5 --- /dev/null +++ b/slither/tools/mutator/README.md @@ -0,0 +1,31 @@ +# Slither-mutate + +`slither-mutate` is a mutation testing tool for solidity based smart contracts. + +## Usage + +`slither-mutate ` + +### CLI Interface + +``` +positional arguments: + codebase Codebase to analyze (.sol file, truffle directory, ...) + test-cmd Command to run the tests for your project + +options: + -h, --help show this help message and exit + --list-mutators List available detectors + --test-dir TEST_DIR Tests directory + --ignore-dirs IGNORE_DIRS + Directories to ignore + --timeout TIMEOUT Set timeout for test command (by default 30 seconds) + --output-dir OUTPUT_DIR + Name of output Directory (by default 'mutation_campaign') + --verbose output all mutants generated + --mutators-to-run MUTATORS_TO_RUN + mutant generators to run + --contract-names CONTRACT_NAMES + list of contract names you want to mutate + --quick to stop full mutation if revert mutator passes +``` \ No newline at end of file diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 80d025c97..b375e9558 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -41,14 +41,14 @@ def parse_args() -> argparse.Namespace: # argument to add the test command parser.add_argument( - "--test-cmd", - help="Command line needed to run the tests for your project" + "test-cmd", + help="Command to run the tests for your project" ) # argument to add the test directory - containing all the tests parser.add_argument( "--test-dir", - help="Directory of tests" + help="Tests directory" ) # argument to ignore the interfaces, libraries @@ -66,7 +66,7 @@ def parse_args() -> argparse.Namespace: # output directory argument parser.add_argument( "--output-dir", - help="Output Directory (by default 'mutation_campaign')" + help="Name of output directory (by default 'mutation_campaign')" ) # to print just all the mutants @@ -170,6 +170,7 @@ def main() -> None: # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) + # insert RR and CR in front of the list CR_RR_list = [] duplicate_list = mutators_list.copy() for M in duplicate_list: @@ -191,8 +192,9 @@ def main() -> None: total_count = 0 # count of valid mutants v_count = 0 - + # lines those need not be mutated (taken from RR and CR) dont_mutate_lines = [] + # mutation try: for compilation_unit_of_main_file in sl.compilation_units: diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index f11d69db1..558a51451 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -3,7 +3,6 @@ import logging from enum import Enum from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit -# from slither.tools.doctor.utils import snip_section from slither.formatters.utils.patches import apply_patch, create_diff from slither.tools.mutator.utils.testing_generated_mutant import test_patch from slither.utils.colors import yellow @@ -100,6 +99,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + # if RR or CR and valid mutant, add line no. if (self.NAME == 'RR' or self.NAME == 'CR') and flag: self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index 01a142c8d..ffebee9fc 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -104,6 +104,4 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str for i in get_sol_file_list(filename, ignore_paths): sol_file_list.append(i) - return sol_file_list - -# TODO: create a function to delete the commands from the sol file + return sol_file_list \ No newline at end of file From 5fb5f69b8e15480eeb0cd37b22f8123719ebe7e3 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 24 Jan 2024 11:47:55 -0500 Subject: [PATCH 24/41] Updated arguments --- slither/tools/mutator/README.md | 6 +++--- slither/tools/mutator/__main__.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index 11b0b7eb5..c615b0b79 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -4,24 +4,24 @@ ## Usage -`slither-mutate ` +`slither-mutate --test-cmd ` ### CLI Interface ``` positional arguments: codebase Codebase to analyze (.sol file, truffle directory, ...) - test-cmd Command to run the tests for your project options: -h, --help show this help message and exit --list-mutators List available detectors + --test-cmd TEST_CMD Command to run the tests for your project --test-dir TEST_DIR Tests directory --ignore-dirs IGNORE_DIRS Directories to ignore --timeout TIMEOUT Set timeout for test command (by default 30 seconds) --output-dir OUTPUT_DIR - Name of output Directory (by default 'mutation_campaign') + Name of output directory (by default 'mutation_campaign') --verbose output all mutants generated --mutators-to-run MUTATORS_TO_RUN mutant generators to run diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index b375e9558..7bd4f8d79 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -26,7 +26,7 @@ logger.setLevel(logging.INFO) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", - usage="slither-mutate target", + usage="slither-mutate --test-cmd ", ) parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)") @@ -41,7 +41,7 @@ def parse_args() -> argparse.Namespace: # argument to add the test command parser.add_argument( - "test-cmd", + "--test-cmd", help="Command to run the tests for your project" ) From 71f970f0517befa56ae6875f6c7d7711789024dd Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Wed, 24 Jan 2024 14:49:28 -0500 Subject: [PATCH 25/41] Updated mutators --- slither/tools/mutator/README.md | 2 ++ slither/tools/mutator/__main__.py | 2 +- slither/tools/mutator/mutators/AOR.py | 3 +-- slither/tools/mutator/mutators/ASOR.py | 3 +-- slither/tools/mutator/mutators/BOR.py | 3 +-- slither/tools/mutator/mutators/CR.py | 3 +-- slither/tools/mutator/mutators/FHR.py | 3 +-- slither/tools/mutator/mutators/LIR.py | 3 +-- slither/tools/mutator/mutators/LOR.py | 3 +-- slither/tools/mutator/mutators/MIA.py | 3 +-- slither/tools/mutator/mutators/MVIE.py | 3 +-- slither/tools/mutator/mutators/MVIV.py | 3 +-- slither/tools/mutator/mutators/MWA.py | 3 +-- slither/tools/mutator/mutators/ROR.py | 3 +-- slither/tools/mutator/mutators/RR.py | 4 +--- slither/tools/mutator/mutators/SBR.py | 3 +-- slither/tools/mutator/mutators/UOR.py | 3 +-- .../mutator/mutators/abstract_mutator.py | 24 +------------------ slither/tools/mutator/utils/command_line.py | 13 +++++----- 19 files changed, 25 insertions(+), 62 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index c615b0b79..30b83ac46 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -6,6 +6,8 @@ `slither-mutate --test-cmd ` +To view the list of mutators available `slither-mutate --list-mutators` + ### CLI Interface ``` diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 7bd4f8d79..13e78a30e 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -118,7 +118,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any ) -> None: # pylint: disable=signature-differs - checks = _get_mutators() + checks = _get_mutators(None) output_mutators(checks) parser.exit() diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 11593ee37..0c7f906b3 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperation arithmetic_operators = [ @@ -15,7 +15,6 @@ arithmetic_operators = [ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 29a6dc672..551cf8c64 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation assignment_operators = [ @@ -20,7 +20,6 @@ assignment_operators = [ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 6a5552a5f..b6a9ae749 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator bitwise_operators = [ BinaryType.AND, @@ -14,7 +14,6 @@ bitwise_operators = [ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 79a63a88f..5c34416e9 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator class CR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "CR" HELP = 'Comment Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 84bfebe22..847466611 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,6 +1,6 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator import re function_header_replacements = [ @@ -13,7 +13,6 @@ function_header_replacements = [ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "FHR" HELP = 'Function Header Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index e8217e935..29efe6502 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line from slither.core.solidity_types import ElementaryType @@ -10,7 +10,6 @@ literal_replacements = [] class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index e14c85708..a8fff1c31 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator logical_operators = [ BinaryType.OROR, @@ -11,7 +11,6 @@ logical_operators = [ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" HELP = "Logical Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 421f94a5e..592014349 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 4ae387d29..b562ba9b0 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index 68bc2f2d8..dcb5be259 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.expressions import Literal from slither.core.variables.variable import Variable -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 4c1e91b32..b482ab566 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -1,13 +1,12 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation class MWA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MWA" HELP = '"while" construct around statement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index da75a592a..826b15a4a 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.slithir.operations import Binary, BinaryType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator relational_operators = [ BinaryType.LESS, @@ -15,7 +15,6 @@ relational_operators = [ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" HELP = "Relational Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 3aab255ec..5afe18892 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -1,13 +1,11 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature - +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator class RR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "RR" HELP = 'Revert Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index f6adfff84..0e9432908 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator import re from slither.core.variables.variable import Variable @@ -47,7 +47,6 @@ solidity_rules = [ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" HELP = 'Solidity Based Replacement' - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index fa00b52ae..cf62c21bd 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -1,7 +1,7 @@ from typing import Dict from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation from slither.tools.mutator.utils.patch import create_patch_with_line -from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator, FaultNature +from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator unary_operators = [ UnaryOperationType.PLUSPLUS_PRE, @@ -14,7 +14,6 @@ unary_operators = [ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOR" HELP = "Unary Operator Replacement" - FAULTNATURE = FaultNature.Missing def _mutate(self) -> Dict: result: Dict = {} diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 558a51451..ea331e7c5 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -1,6 +1,5 @@ import abc import logging -from enum import Enum from typing import Optional, Dict, Tuple, List from slither.core.compilation_unit import SlitherCompilationUnit from slither.formatters.utils.patches import apply_patch, create_diff @@ -12,22 +11,10 @@ logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): pass - -class FaultNature(Enum): - Missing = 0 - Wrong = 1 - Extraneous = 2 - Undefined = 100 - - # not executed - can be detected by replacing with revert - # has no effect - can be detected by removing a line / comment - # can have valid mutant - # can't have valid mutant class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" - FAULTNATURE = FaultNature.Undefined VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 @@ -69,11 +56,6 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- f"HELP is not initialized {self.__class__.__name__}" ) - if self.FAULTNATURE == FaultNature.Undefined: - raise IncorrectMutatorInitialization( - f"FAULTNATURE is not initialized {self.__class__.__name__}" - ) - if rate < 0 or rate > 100: raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" @@ -115,8 +97,4 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) - - - - \ No newline at end of file + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) \ No newline at end of file diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 042b4fff7..358586688 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -7,14 +7,13 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: for detector in mutators_classes: argument = detector.NAME help_info = detector.HELP - fault_nature = detector.FAULTNATURE.name - mutators_list.append((argument, help_info, fault_nature)) - table = MyPrettyTable(["Num", "Name", "What it Does", "Fault Nature"]) + mutators_list.append((argument, help_info)) + table = MyPrettyTable(["Num", "Name", "What it Does"]) - # Sort by class, nature, name - mutators_list = sorted(mutators_list, key=lambda element: (element[2], element[0])) + # Sort by class + mutators_list = sorted(mutators_list, key=lambda element: (element[0])) idx = 1 - for (argument, help_info, fault_nature) in mutators_list: - table.add_row([str(idx), argument, help_info, fault_nature]) + for (argument, help_info) in mutators_list: + table.add_row([str(idx), argument, help_info]) idx = idx + 1 print(table) From c8462b24339a36b3c9faa48524fe184d1ce844dc Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 25 Jan 2024 12:46:34 -0500 Subject: [PATCH 26/41] Updated files --- slither/tools/mutator/README.md | 8 +-- slither/tools/mutator/__main__.py | 66 ++++++++++--------- slither/tools/mutator/mutators/AOR.py | 6 +- slither/tools/mutator/mutators/ASOR.py | 6 +- slither/tools/mutator/mutators/BOR.py | 4 +- slither/tools/mutator/mutators/CR.py | 13 +--- slither/tools/mutator/mutators/FHR.py | 13 ++-- slither/tools/mutator/mutators/LIR.py | 7 +- slither/tools/mutator/mutators/LOR.py | 4 +- slither/tools/mutator/mutators/MIA.py | 7 +- slither/tools/mutator/mutators/MVIE.py | 3 +- slither/tools/mutator/mutators/MVIV.py | 3 +- slither/tools/mutator/mutators/MWA.py | 7 +- slither/tools/mutator/mutators/ROR.py | 3 +- slither/tools/mutator/mutators/RR.py | 10 +-- slither/tools/mutator/mutators/SBR.py | 20 ++---- slither/tools/mutator/mutators/UOR.py | 7 +- .../mutator/mutators/abstract_mutator.py | 30 ++++----- .../tools/mutator/mutators/all_mutators.py | 4 +- slither/tools/mutator/utils/file_handling.py | 60 +++++++++-------- slither/tools/mutator/utils/patch.py | 2 +- .../mutator/utils/testing_generated_mutant.py | 54 ++++++++++----- 22 files changed, 168 insertions(+), 169 deletions(-) diff --git a/slither/tools/mutator/README.md b/slither/tools/mutator/README.md index 30b83ac46..8af977b08 100644 --- a/slither/tools/mutator/README.md +++ b/slither/tools/mutator/README.md @@ -1,6 +1,6 @@ # Slither-mutate -`slither-mutate` is a mutation testing tool for solidity based smart contracts. +`slither-mutate` is a mutation testing tool for solidity based smart contracts. ## Usage @@ -10,9 +10,9 @@ To view the list of mutators available `slither-mutate --list-mutators` ### CLI Interface -``` +```shell positional arguments: - codebase Codebase to analyze (.sol file, truffle directory, ...) + codebase Codebase to analyze (.sol file, project directory, ...) options: -h, --help show this help message and exit @@ -30,4 +30,4 @@ options: --contract-names CONTRACT_NAMES list of contract names you want to mutate --quick to stop full mutation if revert mutator passes -``` \ No newline at end of file +``` diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 13e78a30e..56582d6f5 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,14 +4,14 @@ import logging import sys import os import shutil -from typing import Type, List, Any +from typing import Type, List, Any, Optional from crytic_compile import cryticparser from slither import Slither from slither.tools.mutator.mutators import all_mutators +from slither.utils.colors import yellow, magenta from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list -from slither.utils.colors import yellow, magenta logging.basicConfig() logger = logging.getLogger("Slither-Mutate") @@ -24,12 +24,16 @@ logger.setLevel(logging.INFO) ################################################################################### def parse_args() -> argparse.Namespace: + """ + Parse the underlying arguments for the program. + Returns: The arguments for the program. + """ parser = argparse.ArgumentParser( description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597", usage="slither-mutate --test-cmd ", ) - parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)") + parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") parser.add_argument( "--list-mutators", @@ -108,7 +112,7 @@ def parse_args() -> argparse.Namespace: def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] - if not mutators_list is None: + if mutators_list is not None: detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] else: detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] @@ -122,7 +126,6 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods output_mutators(checks) parser.exit() - # endregion ################################################################################### ################################################################################### @@ -130,46 +133,46 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods ################################################################################### ################################################################################### -def main() -> None: +def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments test_command: str = args.test_cmd - test_directory: str = args.test_dir - paths_to_ignore: str | None = args.ignore_dirs - output_dir: str | None = args.output_dir - timeout: int | None = args.timeout - solc_remappings: str | None = args.solc_remaps - verbose: bool = args.verbose - mutators_to_run: List[str] | None = args.mutators_to_run - contract_names: List[str] | None = args.contract_names - quick_flag: bool = args.quick - - print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) + test_directory: Optional[str] = args.test_dir + paths_to_ignore: Optional[str] = args.ignore_dirs + output_dir: Optional[str] = args.output_dir + timeout: Optional[int] = args.timeout + solc_remappings: Optional[str] = args.solc_remaps + verbose: Optional[bool] = args.verbose + mutators_to_run: Optional[List[str]] = args.mutators_to_run + contract_names: Optional[List[str]] = args.contract_names + quick_flag: Optional[bool] = args.quick + + logger.info(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) if paths_to_ignore: paths_to_ignore_list = paths_to_ignore.strip('][').split(',') - print(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) + logger.info(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) else: paths_to_ignore_list = [] - # get all the contracts as a list from given codebase + # get all the contracts as a list from given codebase sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list) # folder where backup files and valid mutants created - if output_dir == None: + if output_dir is None: output_dir = "/mutation_campaign" output_folder = os.getcwd() + output_dir if os.path.exists(output_folder): shutil.rmtree(output_folder) # set default timeout - if timeout == None: + if timeout is None: timeout = 30 # setting RR mutator as first mutator mutators_list = _get_mutators(mutators_to_run) - + # insert RR and CR in front of the list CR_RR_list = [] duplicate_list = mutators_list.copy() @@ -178,15 +181,15 @@ def main() -> None: mutators_list.remove(M) CR_RR_list.insert(0,M) elif M.NAME == "CR": - mutators_list.remove(M) + mutators_list.remove(M) CR_RR_list.insert(1,M) mutators_list = CR_RR_list + mutators_list - for filename in sol_file_list: + for filename in sol_file_list: # pylint: disable=too-many-nested-blocks contract_name = os.path.split(filename)[1].split('.sol')[0] # slither object sl = Slither(filename, **vars(args)) - # create a backup files + # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) # total count of mutants total_count = 0 @@ -200,7 +203,7 @@ def main() -> None: for compilation_unit_of_main_file in sl.compilation_units: contract_instance = '' for contract in compilation_unit_of_main_file.contracts: - if contract_names != None and contract.name in contract_names: + if contract_names is not None and contract.name in contract_names: contract_instance = contract elif str(contract.name).lower() == contract_name.lower(): contract_instance = contract @@ -215,20 +218,19 @@ def main() -> None: dont_mutate_lines = lines_list if not quick_flag: dont_mutate_lines = [] - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(e) except KeyboardInterrupt: # transfer and delete the backup files if interrupted logger.error("\nExecution interrupted by user (Ctrl + C). Cleaning up...") transfer_and_delete(files_dict) - + # transfer and delete the backup files transfer_and_delete(files_dict) - + # output - print(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) + logger.info(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) - print(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) + logger.info(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) # endregion - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 0c7f906b3..00506a1bb 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -18,11 +18,11 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: try: ir_expression = node.expression - except: + except: # pylint: disable=bare-except continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: @@ -40,4 +40,4 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 551cf8c64..0e7452594 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -24,7 +24,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: @@ -33,7 +33,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods alternative_ops = assignment_operators[:] try: alternative_ops.remove(ir.expression.type) - except: + except: # pylint: disable=bare-except continue for op in alternative_ops: if op != ir.expression: @@ -45,4 +45,4 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index b6a9ae749..9f8ca2a11 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -18,7 +18,7 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in bitwise_operators: @@ -34,4 +34,4 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 5c34416e9..296028236 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -11,22 +11,15 @@ class CR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: new_str = "//" + old_str create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 847466611..4560db4da 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -1,7 +1,8 @@ from typing import Dict +import re from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -import re + function_header_replacements = [ "pure ==> view", @@ -16,9 +17,8 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - - for function in self.contract.functions_and_modifiers_declared: - # function_header = function.source_mapping.content.split('{')[0] + + for function in self.contract.functions_and_modifiers_declared: start = function.source_mapping.start stop = start + function.source_mapping.content.find('{') old_str = self.in_file_str[start:stop] @@ -27,8 +27,7 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods for value in function_header_replacements: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 29efe6502..f9ef874cd 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -11,12 +11,12 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - def _mutate(self) -> Dict: + def _mutate(self) -> Dict: # pylint: disable=too-many-branches result: Dict = {} variable: Variable # Create fault for state variables declaration - for variable in self.contract.state_variables_declared: + for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: @@ -50,7 +50,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods line_no[0] ) - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): @@ -78,5 +78,4 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods new_str, line_no[0] ) - return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index a8fff1c31..21837d81e 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -15,7 +15,7 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -32,4 +32,4 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index 592014349..a10ce1f1d 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -10,7 +10,6 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IF: @@ -24,9 +23,9 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods for value in ["true", "false"]: new_str = value create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - + if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index b562ba9b0..678b3b747 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -56,5 +56,4 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods new_str, line_no[0] ) - - return result + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index dcb5be259..c82f132d7 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -55,5 +55,4 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods new_str, line_no[0] ) - - return result + return result \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index b482ab566..20447290a 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -10,7 +10,7 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - + for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: if node.type == NodeType.IFLOOP: @@ -22,5 +22,6 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: if not isinstance(node.expression, UnaryOperation): new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - return result \ No newline at end of file + create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + return result + \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index 826b15a4a..d59e7575a 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -19,7 +19,7 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: @@ -37,4 +37,3 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 5afe18892..6bfd644e0 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -12,20 +12,14 @@ class RR(AbstractMutator): # pylint: disable=too-few-public-methods for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: if old_str != 'revert()': new_str = 'revert()' create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 0e9432908..bbdc63194 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -1,8 +1,8 @@ from typing import Dict +import re from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -import re from slither.core.variables.variable import Variable solidity_rules = [ @@ -43,19 +43,17 @@ solidity_rules = [ "while ==> if", ] - class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" HELP = 'Solidity Based Replacement' def _mutate(self) -> Dict: - result: Dict = {} variable: Variable - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: - if node.type != NodeType.ENTRYPOINT and node.type != NodeType.ENDIF and node.type != NodeType.ENDLOOP: + if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length @@ -65,11 +63,11 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - for variable in self.contract.state_variables_declared: + for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks node = variable.node_initialization if node: start = node.source_mapping.start @@ -80,13 +78,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods for value in solidity_rules: left_value = value.split(" ==> ")[0] right_value = value.split(" ==> ")[1] - if re.search(re.compile(left_value), old_str) != None: + if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) return result - - - - - - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index cf62c21bd..b7e8c8156 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -18,11 +18,11 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: + for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks for node in function.nodes: try: ir_expression = node.expression - except: + except: # pylint: disable=bare-except continue start = node.source_mapping.start stop = start + node.source_mapping.length @@ -47,5 +47,4 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) new_str = str(variable_read) + str(op) create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) - - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index ea331e7c5..c2f15cd74 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -11,24 +11,24 @@ logger = logging.getLogger("Slither-Mutate") class IncorrectMutatorInitialization(Exception): pass - + class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods NAME = "" HELP = "" VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 - def __init__( - self, compilation_unit: SlitherCompilationUnit, - timeout: int, - testing_command: str, - testing_directory: str, - contract_instance: Contract, - solc_remappings: str | None, + def __init__( # pylint: disable=too-many-arguments + self, compilation_unit: SlitherCompilationUnit, + timeout: int, + testing_command: str, + testing_directory: str, + contract_instance: Contract, + solc_remappings: str | None, verbose: bool, output_folder: str, dont_mutate_line: List[int], - rate: int = 10, + rate: int = 10, seed: Optional[int] = None ) -> None: self.compilation_unit = compilation_unit @@ -60,7 +60,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- raise IncorrectMutatorInitialization( f"rate must be between 0 and 100 {self.__class__.__name__}" ) - + @abc.abstractmethod def _mutate(self) -> Dict: """TODO Documentation""" @@ -70,19 +70,19 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # call _mutate function from different mutators (all_patches) = self._mutate() if "patches" not in all_patches: - logger.debug(f"No patches found by {self.NAME}") + logger.debug("No patches found by %s", self.NAME) return (0,0,self.dont_mutate_line) - + for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") patches = all_patches["patches"][file] patches.sort(key=lambda x: x["start"]) - print(yellow(f"Mutating {file} with {self.NAME} \n")) + logger.info(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) # if RR or CR and valid mutant, add line no. - if (self.NAME == 'RR' or self.NAME == 'CR') and flag: + if self.NAME in ('RR', 'CR') and flag: self.dont_mutate_line.append(patch['line_number']) # count the valid and invalid mutants if not flag: @@ -97,4 +97,4 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- # add valid mutant patches to a output file with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) \ No newline at end of file + return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 21925317d..852a8efc3 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -3,7 +3,7 @@ from slither.tools.mutator.mutators.MVIV import MVIV # severity low from slither.tools.mutator.mutators.MVIE import MVIE # severity low from slither.tools.mutator.mutators.LOR import LOR # severity medium from slither.tools.mutator.mutators.UOR import UOR # severity medium -from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium from slither.tools.mutator.mutators.AOR import AOR # severity medium from slither.tools.mutator.mutators.BOR import BOR # severity medium from slither.tools.mutator.mutators.ASOR import ASOR # severity medium @@ -13,4 +13,4 @@ from slither.tools.mutator.mutators.FHR import FHR # severity medium from slither.tools.mutator.mutators.MIA import MIA # severity medium from slither.tools.mutator.mutators.ROR import ROR # severity medium from slither.tools.mutator.mutators.RR import RR # severity high -from slither.tools.mutator.mutators.CR import CR # severity high \ No newline at end of file +from slither.tools.mutator.mutators.CR import CR # severity high diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index ffebee9fc..d4474c310 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,47 +6,49 @@ logger = logging.getLogger("Slither-Mutate") duplicated_files = {} -# function to backup the source file def backup_source_file(source_code: Dict, output_folder: str) -> Dict: + """ + function to backup the source file + returns: dictionary of duplicated files + """ os.makedirs(output_folder, exist_ok=True) - + for file_path, content in source_code.items(): directory, filename = os.path.split(file_path) new_filename = f"{output_folder}/backup_{filename}" new_file_path = os.path.join(directory, new_filename) - with open(new_file_path, 'w') as new_file: + with open(new_file_path, 'w', encoding="utf8") as new_file: new_file.write(content) duplicated_files[file_path] = new_file_path return duplicated_files -# function to transfer the original content to the sol file after campaign def transfer_and_delete(files_dict: Dict) -> None: + """function to transfer the original content to the sol file after campaign""" try: files_dict_copy = files_dict.copy() - for item, value in files_dict_copy.items(): - with open(value, 'r') as duplicated_file: + for item, value in files_dict_copy.items(): + with open(value, 'r', encoding="utf8") as duplicated_file: content = duplicated_file.read() - with open(item, 'w') as original_file: + with open(item, 'w', encoding="utf8") as original_file: original_file.write(content) os.remove(value) # delete elements from the global dict del duplicated_files[item] - - except Exception as e: + + except Exception as e: # pylint: disable=broad-except logger.error(f"Error transferring content: {e}") -#function to create new mutant file def create_mutant_file(file: str, count: int, rule: str) -> None: - + """function to create new mutant file""" try: - directory, filename = os.path.split(file) + _, filename = os.path.split(file) # Read content from the duplicated file - with open(file, 'r') as source_file: + with open(file, 'r', encoding="utf8") as source_file: content = source_file.read() # Write content to the original file @@ -54,42 +56,46 @@ def create_mutant_file(file: str, count: int, rule: str) -> None: # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file: + with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w', encoding="utf8") as mutant_file: mutant_file.write(content) # reset the file - with open(duplicated_files[file], 'r') as duplicated_file: + with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w') as source_file: + with open(file, 'w', encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(f"Error creating mutant: {e}") -# function to reset the file def reset_file(file: str) -> None: + """function to reset the file""" try: # directory, filename = os.path.split(file) # reset the file - with open(duplicated_files[file], 'r') as duplicated_file: + with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w') as source_file: + with open(file, 'w', encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: + except Exception as e: # pylint: disable=broad-except logger.error(f"Error resetting file: {e}") -# function to get the contracts list + def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]: + """ + function to get the contracts list + returns: list of .sol files + """ sol_file_list = [] - if ignore_paths == None: + if ignore_paths is None: ignore_paths = [] # if input is contract file if os.path.isfile(codebase): return [codebase] - + # if input is folder elif os.path.isdir(codebase): directory = os.path.abspath(codebase) @@ -98,10 +104,10 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str if os.path.isfile(filename): sol_file_list.append(filename) elif os.path.isdir(filename): - directory_name, dirname = os.path.split(filename) + _, dirname = os.path.split(filename) if dirname in ignore_paths: - continue + continue for i in get_sol_file_list(filename, ignore_paths): sol_file_list.append(i) - return sol_file_list \ No newline at end of file + return sol_file_list diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py index 39c77f673..e50f44612 100644 --- a/slither/tools/mutator/utils/patch.py +++ b/slither/tools/mutator/utils/patch.py @@ -19,4 +19,4 @@ def create_patch_with_line( if "patches" not in result: result["patches"] = defaultdict(list) if p not in result["patches"][file]: - result["patches"][file].append(p) \ No newline at end of file + result["patches"][file].append(p) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 8525e2808..5c75f0eb3 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -1,34 +1,48 @@ -import crytic_compile import subprocess import os import logging import time import signal from typing import Dict +import crytic_compile from slither.tools.mutator.utils.file_handling import create_mutant_file, reset_file from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") -# function to compile the generated mutant def compile_generated_mutant(file_path: str, mappings: str) -> bool: + """ + function to compile the generated mutant + returns: status of compilation + """ try: crytic_compile.CryticCompile(file_path, solc_remaps=mappings) return True - except: # pylint: disable=broad-except + except: # pylint: disable=bare-except return False - + def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + """ + function to run codebase tests + returns: boolean whether the tests passed or not + """ # add --fail-fast for foundry tests, to exit after first failure - if "forge test" in cmd and not "--fail-fast" in cmd: + if "forge test" in cmd and "--fail-fast" not in cmd: cmd += " --fail-fast" # add --bail for hardhat and truffle tests, to exit after first failure - elif "hardhat test" in cmd or "truffle test" and not "--bail" in cmd: + elif "hardhat test" in cmd or "truffle test" in cmd and "--bail" not in cmd: cmd += " --bail" start = time.time() + # starting new process - P = subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) + P = subprocess.Popen( + [cmd], + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid + ) try: # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: @@ -37,30 +51,34 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: if P.poll() is None: logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") # sends a SIGTERM signal to process group - bascially killing the process - os.killpg(os.getpgid(P.pid), signal.SIGTERM) + os.killpg(os.getpgid(P.pid), signal.SIGTERM) # Avoid any weird race conditions from grabbing the return code time.sleep(0.05) # indicates whether the command executed sucessfully or not r = P.returncode # if r is 0 then it is valid mutant because tests didn't fail - return True if r == 0 else False + return r == 0 -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: - with open(file, 'r') as filepath: +def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: # pylint: disable=too-many-arguments + """ + function to verify the validity of each patch + returns: valid or invalid patch + """ + with open(file, 'r', encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] # Write the modified content back to the file - with open(file, 'w') as filepath: + with open(file, 'w', encoding="utf-8") as filepath: filepath.write(replaced_content) - if(compile_generated_mutant(file, mappings)): - if(run_test_cmd(command, file, timeout)): + if compile_generated_mutant(file, mappings): + if run_test_cmd(command, file, timeout): create_mutant_file(file, index, generator_name) - logger.info(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + print(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) return True - + reset_file(file) if verbose: - logger.info(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) - return False \ No newline at end of file + print(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) + return False From f0bb9d35674c822e3397392a912ecef4a7f40d12 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Thu, 25 Jan 2024 19:22:08 -0500 Subject: [PATCH 27/41] Updated with formatting --- slither/tools/mutator/__main__.py | 90 +++++++++++------- slither/tools/mutator/mutators/AOR.py | 19 +++- slither/tools/mutator/mutators/ASOR.py | 29 ++++-- slither/tools/mutator/mutators/BOR.py | 17 +++- slither/tools/mutator/mutators/CR.py | 22 ++++- slither/tools/mutator/mutators/FHR.py | 21 +++-- slither/tools/mutator/mutators/LIR.py | 35 ++++--- slither/tools/mutator/mutators/LOR.py | 15 ++- slither/tools/mutator/mutators/MIA.py | 24 ++++- slither/tools/mutator/mutators/MVIE.py | 11 ++- slither/tools/mutator/mutators/MVIV.py | 7 +- slither/tools/mutator/mutators/MWA.py | 14 ++- slither/tools/mutator/mutators/ROR.py | 20 +++- slither/tools/mutator/mutators/RR.py | 23 ++++- slither/tools/mutator/mutators/SBR.py | 93 ++++++++++++------- slither/tools/mutator/mutators/UOR.py | 58 ++++++++++-- .../mutator/mutators/abstract_mutator.py | 48 +++++++--- .../tools/mutator/mutators/all_mutators.py | 30 +++--- slither/tools/mutator/utils/command_line.py | 3 +- slither/tools/mutator/utils/file_handling.py | 45 ++++++--- slither/tools/mutator/utils/patch.py | 11 ++- .../mutator/utils/testing_generated_mutant.py | 74 +++++++++------ 22 files changed, 504 insertions(+), 205 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 56582d6f5..f0b5c88bc 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -11,7 +11,11 @@ from slither.tools.mutator.mutators import all_mutators from slither.utils.colors import yellow, magenta from .mutators.abstract_mutator import AbstractMutator from .utils.command_line import output_mutators -from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list +from .utils.file_handling import ( + transfer_and_delete, + backup_source_file, + get_sol_file_list, +) logging.basicConfig() logger = logging.getLogger("Slither-Mutate") @@ -23,6 +27,7 @@ logger.setLevel(logging.INFO) ################################################################################### ################################################################################### + def parse_args() -> argparse.Namespace: """ Parse the underlying arguments for the program. @@ -33,7 +38,9 @@ def parse_args() -> argparse.Namespace: usage="slither-mutate --test-cmd ", ) - parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") + parser.add_argument( + "codebase", help="Codebase to analyze (.sol file, project directory, ...)" + ) parser.add_argument( "--list-mutators", @@ -44,33 +51,22 @@ def parse_args() -> argparse.Namespace: ) # argument to add the test command - parser.add_argument( - "--test-cmd", - help="Command to run the tests for your project" - ) + parser.add_argument("--test-cmd", help="Command to run the tests for your project") # argument to add the test directory - containing all the tests - parser.add_argument( - "--test-dir", - help="Tests directory" - ) + parser.add_argument("--test-dir", help="Tests directory") # argument to ignore the interfaces, libraries - parser.add_argument( - "--ignore-dirs", - help="Directories to ignore" - ) + parser.add_argument("--ignore-dirs", help="Directories to ignore") # time out argument parser.add_argument( - "--timeout", - help="Set timeout for test command (by default 30 seconds)" + "--timeout", help="Set timeout for test command (by default 30 seconds)" ) # output directory argument parser.add_argument( - "--output-dir", - help="Name of output directory (by default 'mutation_campaign')" + "--output-dir", help="Name of output directory (by default 'mutation_campaign')" ) # to print just all the mutants @@ -110,14 +106,26 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() + def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] if mutators_list is not None: - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ] + detectors = [ + c + for c in detectors_ + if inspect.isclass(c) + and issubclass(c, AbstractMutator) + and str(c.NAME) in mutators_list + ] else: - detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ] + detectors = [ + c + for c in detectors_ + if inspect.isclass(c) and issubclass(c, AbstractMutator) + ] return detectors + class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods def __call__( self, parser: Any, *args: Any, **kwargs: Any @@ -126,6 +134,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods output_mutators(checks) parser.exit() + # endregion ################################################################################### ################################################################################### @@ -133,7 +142,10 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods ################################################################################### ################################################################################### -def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals + +def main() -> ( + None +): # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments @@ -151,7 +163,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- logger.info(magenta(f"Starting Mutation Campaign in '{args.codebase} \n")) if paths_to_ignore: - paths_to_ignore_list = paths_to_ignore.strip('][').split(',') + paths_to_ignore_list = paths_to_ignore.strip("][").split(",") logger.info(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n")) else: paths_to_ignore_list = [] @@ -179,14 +191,14 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- for M in duplicate_list: if M.NAME == "RR": mutators_list.remove(M) - CR_RR_list.insert(0,M) + CR_RR_list.insert(0, M) elif M.NAME == "CR": mutators_list.remove(M) - CR_RR_list.insert(1,M) + CR_RR_list.insert(1, M) mutators_list = CR_RR_list + mutators_list - for filename in sol_file_list: # pylint: disable=too-many-nested-blocks - contract_name = os.path.split(filename)[1].split('.sol')[0] + for filename in sol_file_list: # pylint: disable=too-many-nested-blocks + contract_name = os.path.split(filename)[1].split(".sol")[0] # slither object sl = Slither(filename, **vars(args)) # create a backup files @@ -201,24 +213,34 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- # mutation try: for compilation_unit_of_main_file in sl.compilation_units: - contract_instance = '' + contract_instance = "" for contract in compilation_unit_of_main_file.contracts: if contract_names is not None and contract.name in contract_names: contract_instance = contract elif str(contract.name).lower() == contract_name.lower(): contract_instance = contract - if contract_instance == '': + if contract_instance == "": logger.error("Can't find the contract") else: for M in mutators_list: - m = M(compilation_unit_of_main_file, int(timeout), test_command, test_directory, contract_instance, solc_remappings, verbose, output_folder, dont_mutate_lines) + m = M( + compilation_unit_of_main_file, + int(timeout), + test_command, + test_directory, + contract_instance, + solc_remappings, + verbose, + output_folder, + dont_mutate_lines, + ) (count_valid, count_invalid, lines_list) = m.mutate() v_count += count_valid total_count += count_valid + count_invalid dont_mutate_lines = lines_list if not quick_flag: dont_mutate_lines = [] - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(e) except KeyboardInterrupt: @@ -230,7 +252,13 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too- transfer_and_delete(files_dict) # output - logger.info(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n")) + logger.info( + yellow( + f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n" + ) + ) logger.info(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n")) + + # endregion diff --git a/slither/tools/mutator/mutators/AOR.py b/slither/tools/mutator/mutators/AOR.py index 00506a1bb..0bf0fb2a2 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -9,20 +9,23 @@ arithmetic_operators = [ BinaryType.DIVISION, BinaryType.MULTIPLICATION, BinaryType.SUBTRACTION, - BinaryType.MODULO + BinaryType.MODULO, ] + class AOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "AOR" HELP = "Arithmetic operator replacement" def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: ir_expression = node.expression - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue for ir in node.irs: if isinstance(ir, Binary) and ir.type in arithmetic_operators: @@ -39,5 +42,13 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/ASOR.py b/slither/tools/mutator/mutators/ASOR.py index 0e7452594..2ff403b38 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -1,7 +1,10 @@ from typing import Dict from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator -from slither.core.expressions.assignment_operation import AssignmentOperationType, AssignmentOperation +from slither.core.expressions.assignment_operation import ( + AssignmentOperationType, + AssignmentOperation, +) assignment_operators = [ AssignmentOperationType.ASSIGN_ADDITION, @@ -14,9 +17,10 @@ assignment_operators = [ AssignmentOperationType.ASSIGN_RIGHT_SHIFT, AssignmentOperationType.ASSIGN_MULTIPLICATION, AssignmentOperationType.ASSIGN_DIVISION, - AssignmentOperationType.ASSIGN_MODULO + AssignmentOperationType.ASSIGN_MODULO, ] + class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ASOR" HELP = "Assignment Operator Replacement" @@ -24,16 +28,21 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: - if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators: + if ( + isinstance(ir.expression, AssignmentOperation) + and ir.expression.type in assignment_operators + ): if ir.expression.type == AssignmentOperationType.ASSIGN: continue alternative_ops = assignment_operators[:] try: alternative_ops.remove(ir.expression.type) - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue for op in alternative_ops: if op != ir.expression: @@ -44,5 +53,13 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/BOR.py b/slither/tools/mutator/mutators/BOR.py index 9f8ca2a11..a8720a4b6 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -8,9 +8,10 @@ bitwise_operators = [ BinaryType.OR, BinaryType.LEFT_SHIFT, BinaryType.RIGHT_SHIFT, - BinaryType.CARET + BinaryType.CARET, ] + class BOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "BOR" HELP = "Bitwise Operator Replacement" @@ -18,7 +19,9 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in bitwise_operators: @@ -33,5 +36,13 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/CR.py b/slither/tools/mutator/mutators/CR.py index 296028236..ebf93bf18 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -6,14 +6,20 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator class CR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "CR" - HELP = 'Comment Replacement' + HELP = "Comment Replacement" def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length @@ -21,5 +27,13 @@ class CR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: new_str = "//" + old_str - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/FHR.py b/slither/tools/mutator/mutators/FHR.py index 4560db4da..028c1916c 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -7,27 +7,36 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator function_header_replacements = [ "pure ==> view", "view ==> pure", - "(\s)(external|public|internal) ==> \\1private", - "(\s)(external|public) ==> \\1internal" + "(\\s)(external|public|internal) ==> \\1private", + "(\\s)(external|public) ==> \\1internal", ] + class FHR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "FHR" - HELP = 'Function Header Replacement' + HELP = "Function Header Replacement" def _mutate(self) -> Dict: result: Dict = {} for function in self.contract.functions_and_modifiers_declared: start = function.source_mapping.start - stop = start + function.source_mapping.content.find('{') + stop = start + function.source_mapping.content.find("{") old_str = self.in_file_str[start:stop] line_no = function.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in function_header_replacements: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index f9ef874cd..066a1fbb0 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -7,16 +7,19 @@ from slither.core.solidity_types import ElementaryType literal_replacements = [] + class LIR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LIR" HELP = "Literal Interger Replacement" - def _mutate(self) -> Dict: # pylint: disable=too-many-branches + def _mutate(self) -> Dict: # pylint: disable=too-many-branches result: Dict = {} variable: Variable # Create fault for state variables declaration - for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + variable + ) in self.contract.state_variables_declared: if variable.initialized: # Cannot remove the initialization of constant variables if variable.is_constant: @@ -24,12 +27,16 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods if isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): - literal_replacements.append(variable.type.min) # append data type min value - literal_replacements.append(variable.type.max) # append data type max value + literal_replacements.append( + variable.type.min + ) # append data type min value + literal_replacements.append( + variable.type.max + ) # append data type max value if str(variable.type).startswith("uint"): - literal_replacements.append('1') + literal_replacements.append("1") elif str(variable.type).startswith("uint"): - literal_replacements.append('-1') + literal_replacements.append("-1") # Get the string start = variable.source_mapping.start stop = start + variable.source_mapping.length @@ -37,7 +44,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods line_no = variable.node_initialization.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() + old_value = old_str[old_str.find("=") + 1 :].strip() if old_value != value: new_str = f"{old_str.split('=')[0]}= {value}" create_patch_with_line( @@ -47,26 +54,28 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods stop, old_str, new_str, - line_no[0] + line_no[0], ) - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: if variable.initialized and isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): literal_replacements.append(variable.type.min) literal_replacements.append(variable.type.max) if str(variable.type).startswith("uint"): - literal_replacements.append('1') + literal_replacements.append("1") elif str(variable.type).startswith("uint"): - literal_replacements.append('-1') + literal_replacements.append("-1") start = variable.source_mapping.start stop = start + variable.source_mapping.length old_str = self.in_file_str[start:stop] line_no = variable.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for new_value in literal_replacements: - old_value = old_str[old_str.find("=")+1:].strip() + old_value = old_str[old_str.find("=") + 1 :].strip() if old_value != new_value: new_str = f"{old_str.split('=')[0]}= {new_value}" create_patch_with_line( @@ -76,6 +85,6 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods stop, old_str, new_str, - line_no[0] + line_no[0], ) return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index 21837d81e..2d1535b1a 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -8,6 +8,7 @@ logical_operators = [ BinaryType.ANDAND, ] + class LOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "LOR" HELP = "Logical Operator Replacement" @@ -15,7 +16,9 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in logical_operators: @@ -31,5 +34,13 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/MIA.py b/slither/tools/mutator/mutators/MIA.py index a10ce1f1d..f29569f63 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -4,6 +4,7 @@ from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + class MIA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MIA" HELP = '"if" construct around statement' @@ -22,10 +23,25 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods # Replace the expression with true and false for value in ["true", "false"]: new_str = value - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(UnaryOperationType.BANG) + "(" + old_str + ")" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 678b3b747..4d5f6205d 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -4,6 +4,7 @@ from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line + class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIE" HELP = "variable initialization using an expression" @@ -34,12 +35,14 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: - if variable.initialized and not isinstance(variable.expression, Literal): + if variable.initialized and not isinstance( + variable.expression, Literal + ): # Get the string start = variable.source_mapping.start stop = variable.expression.source_mapping.start @@ -54,6 +57,6 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MVIV.py b/slither/tools/mutator/mutators/MVIV.py index c82f132d7..f9e51c553 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,6 +4,7 @@ from slither.core.variables.variable import Variable from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.tools.mutator.utils.patch import create_patch_with_line + class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MVIV" HELP = "variable initialization using a value" @@ -34,7 +35,7 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) for function in self.contract.functions_and_modifiers_declared: @@ -53,6 +54,6 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods stop + variable.expression.source_mapping.length, old_str, new_str, - line_no[0] + line_no[0], ) - return result \ No newline at end of file + return result diff --git a/slither/tools/mutator/mutators/MWA.py b/slither/tools/mutator/mutators/MWA.py index 20447290a..9682f10ca 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -4,6 +4,7 @@ from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.expressions.unary_operation import UnaryOperationType, UnaryOperation + class MWA(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "MWA" HELP = '"while" construct around statement' @@ -21,7 +22,14 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: if not isinstance(node.expression, UnaryOperation): - new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = str(UnaryOperationType.BANG) + "(" + old_str + ")" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result - \ No newline at end of file diff --git a/slither/tools/mutator/mutators/ROR.py b/slither/tools/mutator/mutators/ROR.py index d59e7575a..9daae0663 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -12,6 +12,7 @@ relational_operators = [ BinaryType.NOT_EQUAL, ] + class ROR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "ROR" HELP = "Relational Operator Replacement" @@ -19,11 +20,16 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: for ir in node.irs: if isinstance(ir, Binary) and ir.type in relational_operators: - if str(ir.variable_left.type) != 'address' and str(ir.variable_right) != 'address': + if ( + str(ir.variable_left.type) != "address" + and str(ir.variable_right) != "address" + ): alternative_ops = relational_operators[:] alternative_ops.remove(ir.type) for op in alternative_ops: @@ -35,5 +41,13 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods if not line_no[0] in self.dont_mutate_line: # Replace the expression with true new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}" - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/RR.py b/slither/tools/mutator/mutators/RR.py index 6bfd644e0..e285d7a3f 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -3,23 +3,36 @@ from slither.core.cfg.node import NodeType from slither.tools.mutator.utils.patch import create_patch_with_line from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator + class RR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "RR" - HELP = 'Revert Replacement' + HELP = "Revert Replacement" def _mutate(self) -> Dict: result: Dict = {} for function in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: - if old_str != 'revert()': - new_str = 'revert()' - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if old_str != "revert()": + new_str = "revert()" + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index bbdc63194..770b57180 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -6,29 +6,29 @@ from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.core.variables.variable import Variable solidity_rules = [ - "abi\.encode\( ==> abi.encodePacked(", - "abi\.encodePacked\( ==> abi.encode(", - "\.call([({]) ==> .delegatecall\\1", - "\.call([({]) ==> .staticcall\\1", - "\.delegatecall([({]) ==> .call\\1", - "\.delegatecall([({]) ==> .staticcall\\1", - "\.staticcall([({]) ==> .delegatecall\\1", - "\.staticcall([({]) ==> .call\\1", + "abi\\.encode\\( ==> abi.encodePacked(", + "abi\\.encodePacked\\( ==> abi.encode(", + "\\.call([({]) ==> .delegatecall\\1", + "\\.call([({]) ==> .staticcall\\1", + "\\.delegatecall([({]) ==> .call\\1", + "\\.delegatecall([({]) ==> .staticcall\\1", + "\\.staticcall([({]) ==> .delegatecall\\1", + "\\.staticcall([({]) ==> .call\\1", "^now$ ==> 0", "block.timestamp ==> 0", "msg.value ==> 0", "msg.value ==> 1", - "(\s)(wei|gwei) ==> \\1ether", - "(\s)(ether|gwei) ==> \\1wei", - "(\s)(wei|ether) ==> \\1gwei", - "(\s)(minutes|days|hours|weeks) ==> \\1seconds", - "(\s)(seconds|days|hours|weeks) ==> \\1minutes", - "(\s)(seconds|minutes|hours|weeks) ==> \\1days", - "(\s)(seconds|minutes|days|weeks) ==> \\1hours", - "(\s)(seconds|minutes|days|hours) ==> \\1weeks", - "(\s)(memory) ==> \\1storage", - "(\s)(storage) ==> \\1memory", - "(\s)(constant) ==> \\1immutable", + "(\\s)(wei|gwei) ==> \\1ether", + "(\\s)(ether|gwei) ==> \\1wei", + "(\\s)(wei|ether) ==> \\1gwei", + "(\\s)(minutes|days|hours|weeks) ==> \\1seconds", + "(\\s)(seconds|days|hours|weeks) ==> \\1minutes", + "(\\s)(seconds|minutes|hours|weeks) ==> \\1days", + "(\\s)(seconds|minutes|days|weeks) ==> \\1hours", + "(\\s)(seconds|minutes|days|hours) ==> \\1weeks", + "(\\s)(memory) ==> \\1storage", + "(\\s)(storage) ==> \\1memory", + "(\\s)(constant) ==> \\1immutable", "addmod ==> mulmod", "mulmod ==> addmod", "msg.sender ==> tx.origin", @@ -40,45 +40,74 @@ solidity_rules = [ "(u?)int64 ==> \\1int32", "(u?)int128 ==> \\1int64", "(u?)int256 ==> \\1int128", - "while ==> if", + "while ==> if", ] + class SBR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "SBR" - HELP = 'Solidity Based Replacement' + HELP = "Solidity Based Replacement" def _mutate(self) -> Dict: result: Dict = {} variable: Variable - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: - if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP): + if node.type not in ( + NodeType.ENTRYPOINT, + NodeType.ENDIF, + NodeType.ENDLOOP, + ): # Get the string start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in solidity_rules: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = re.sub( + re.compile(left_value), right_value, old_str + ) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) - for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + variable + ) in self.contract.state_variables_declared: node = variable.node_initialization if node: start = node.source_mapping.start stop = start + node.source_mapping.length - old_str = self.in_file_str[start:stop] + old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: for value in solidity_rules: - left_value = value.split(" ==> ")[0] + left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub(re.compile(left_value), right_value, old_str) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + new_str = re.sub( + re.compile(left_value), right_value, old_str + ) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/UOR.py b/slither/tools/mutator/mutators/UOR.py index b7e8c8156..f427c2fbf 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -8,9 +8,10 @@ unary_operators = [ UnaryOperationType.MINUSMINUS_PRE, UnaryOperationType.PLUSPLUS_POST, UnaryOperationType.MINUSMINUS_POST, - UnaryOperationType.MINUS_PRE + UnaryOperationType.MINUS_PRE, ] + class UOR(AbstractMutator): # pylint: disable=too-few-public-methods NAME = "UOR" HELP = "Unary Operator Replacement" @@ -18,33 +19,70 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods def _mutate(self) -> Dict: result: Dict = {} - for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks + for ( # pylint: disable=too-many-nested-blocks + function + ) in self.contract.functions_and_modifiers_declared: for node in function.nodes: try: ir_expression = node.expression - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except continue start = node.source_mapping.start stop = start + node.source_mapping.length old_str = self.in_file_str[start:stop] line_no = node.source_mapping.lines if not line_no[0] in self.dont_mutate_line: - if isinstance(ir_expression, UnaryOperation) and ir_expression.type in unary_operators: + if ( + isinstance(ir_expression, UnaryOperation) + and ir_expression.type in unary_operators + ): for op in unary_operators: if not node.expression.is_prefix: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(variable_read) + str(op) - if new_str != old_str and str(op) != '-': - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if new_str != old_str and str(op) != "-": + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) new_str = str(op) + str(variable_read) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) else: if node.expression.type != op: variable_read = node.variables_read[0] new_str = str(op) + str(variable_read) - if new_str != old_str and str(op) != '-': - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + if new_str != old_str and str(op) != "-": + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) new_str = str(variable_read) + str(op) - create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0]) + create_patch_with_line( + result, + self.in_file, + start, + stop, + old_str, + new_str, + line_no[0], + ) return result diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index c2f15cd74..12c6b2298 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -9,17 +9,22 @@ from slither.core.declarations import Contract logger = logging.getLogger("Slither-Mutate") + class IncorrectMutatorInitialization(Exception): pass -class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods + +class AbstractMutator( + metaclass=abc.ABCMeta +): # pylint: disable=too-few-public-methods,too-many-instance-attributes NAME = "" HELP = "" VALID_MUTANTS_COUNT = 0 INVALID_MUTANTS_COUNT = 0 - def __init__( # pylint: disable=too-many-arguments - self, compilation_unit: SlitherCompilationUnit, + def __init__( # pylint: disable=too-many-arguments + self, + compilation_unit: SlitherCompilationUnit, timeout: int, testing_command: str, testing_directory: str, @@ -29,7 +34,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- output_folder: str, dont_mutate_line: List[int], rate: int = 10, - seed: Optional[int] = None + seed: Optional[int] = None, ) -> None: self.compilation_unit = compilation_unit self.slither = compilation_unit.core @@ -71,7 +76,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- (all_patches) = self._mutate() if "patches" not in all_patches: logger.debug("No patches found by %s", self.NAME) - return (0,0,self.dont_mutate_line) + return (0, 0, self.dont_mutate_line) for file in all_patches["patches"]: original_txt = self.slither.source_code[file].encode("utf8") @@ -80,21 +85,38 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public- logger.info(yellow(f"Mutating {file} with {self.NAME} \n")) for patch in patches: # test the patch - flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose) + flag = test_patch( + file, + patch, + self.test_command, + self.VALID_MUTANTS_COUNT, + self.NAME, + self.timeout, + self.solc_remappings, + self.verbose, + ) # if RR or CR and valid mutant, add line no. - if self.NAME in ('RR', 'CR') and flag: - self.dont_mutate_line.append(patch['line_number']) + if self.NAME in ("RR", "CR") and flag: + self.dont_mutate_line.append(patch["line_number"]) # count the valid and invalid mutants if not flag: self.INVALID_MUTANTS_COUNT += 1 continue self.VALID_MUTANTS_COUNT += 1 - patched_txt,_ = apply_patch(original_txt, patch, 0) - diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) + patched_txt, _ = apply_patch(original_txt, patch, 0) + diff = create_diff( + self.compilation_unit, original_txt, patched_txt, file + ) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") # add valid mutant patches to a output file - with open(self.output_folder + "/patches_file.txt", 'a') as patches_file: - patches_file.write(diff + '\n') - return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line) + with open( + self.output_folder + "/patches_file.txt", "a", encoding="utf8" + ) as patches_file: + patches_file.write(diff + "\n") + return ( + self.VALID_MUTANTS_COUNT, + self.INVALID_MUTANTS_COUNT, + self.dont_mutate_line, + ) diff --git a/slither/tools/mutator/mutators/all_mutators.py b/slither/tools/mutator/mutators/all_mutators.py index 852a8efc3..b02a2cc9b 100644 --- a/slither/tools/mutator/mutators/all_mutators.py +++ b/slither/tools/mutator/mutators/all_mutators.py @@ -1,16 +1,16 @@ # pylint: disable=unused-import -from slither.tools.mutator.mutators.MVIV import MVIV # severity low -from slither.tools.mutator.mutators.MVIE import MVIE # severity low -from slither.tools.mutator.mutators.LOR import LOR # severity medium -from slither.tools.mutator.mutators.UOR import UOR # severity medium -from slither.tools.mutator.mutators.SBR import SBR # severity medium -from slither.tools.mutator.mutators.AOR import AOR # severity medium -from slither.tools.mutator.mutators.BOR import BOR # severity medium -from slither.tools.mutator.mutators.ASOR import ASOR # severity medium -from slither.tools.mutator.mutators.MWA import MWA # severity medium -from slither.tools.mutator.mutators.LIR import LIR # severity medium -from slither.tools.mutator.mutators.FHR import FHR # severity medium -from slither.tools.mutator.mutators.MIA import MIA # severity medium -from slither.tools.mutator.mutators.ROR import ROR # severity medium -from slither.tools.mutator.mutators.RR import RR # severity high -from slither.tools.mutator.mutators.CR import CR # severity high +from slither.tools.mutator.mutators.MVIV import MVIV # severity low +from slither.tools.mutator.mutators.MVIE import MVIE # severity low +from slither.tools.mutator.mutators.LOR import LOR # severity medium +from slither.tools.mutator.mutators.UOR import UOR # severity medium +from slither.tools.mutator.mutators.SBR import SBR # severity medium +from slither.tools.mutator.mutators.AOR import AOR # severity medium +from slither.tools.mutator.mutators.BOR import BOR # severity medium +from slither.tools.mutator.mutators.ASOR import ASOR # severity medium +from slither.tools.mutator.mutators.MWA import MWA # severity medium +from slither.tools.mutator.mutators.LIR import LIR # severity medium +from slither.tools.mutator.mutators.FHR import FHR # severity medium +from slither.tools.mutator.mutators.MIA import MIA # severity medium +from slither.tools.mutator.mutators.ROR import ROR # severity medium +from slither.tools.mutator.mutators.RR import RR # severity high +from slither.tools.mutator.mutators.CR import CR # severity high diff --git a/slither/tools/mutator/utils/command_line.py b/slither/tools/mutator/utils/command_line.py index 358586688..79d705097 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -2,6 +2,7 @@ from typing import List, Type from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator from slither.utils.myprettytable import MyPrettyTable + def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: mutators_list = [] for detector in mutators_classes: @@ -13,7 +14,7 @@ def output_mutators(mutators_classes: List[Type[AbstractMutator]]) -> None: # Sort by class mutators_list = sorted(mutators_list, key=lambda element: (element[0])) idx = 1 - for (argument, help_info) in mutators_list: + for argument, help_info in mutators_list: table.add_row([str(idx), argument, help_info]) idx = idx + 1 print(table) diff --git a/slither/tools/mutator/utils/file_handling.py b/slither/tools/mutator/utils/file_handling.py index d4474c310..ddb3efb50 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,6 +6,7 @@ logger = logging.getLogger("Slither-Mutate") duplicated_files = {} + def backup_source_file(source_code: Dict, output_folder: str) -> Dict: """ function to backup the source file @@ -18,21 +19,22 @@ def backup_source_file(source_code: Dict, output_folder: str) -> Dict: new_filename = f"{output_folder}/backup_{filename}" new_file_path = os.path.join(directory, new_filename) - with open(new_file_path, 'w', encoding="utf8") as new_file: + with open(new_file_path, "w", encoding="utf8") as new_file: new_file.write(content) duplicated_files[file_path] = new_file_path return duplicated_files + def transfer_and_delete(files_dict: Dict) -> None: """function to transfer the original content to the sol file after campaign""" try: files_dict_copy = files_dict.copy() for item, value in files_dict_copy.items(): - with open(value, 'r', encoding="utf8") as duplicated_file: + with open(value, "r", encoding="utf8") as duplicated_file: content = duplicated_file.read() - with open(item, 'w', encoding="utf8") as original_file: + with open(item, "w", encoding="utf8") as original_file: original_file.write(content) os.remove(value) @@ -40,47 +42,61 @@ def transfer_and_delete(files_dict: Dict) -> None: # delete elements from the global dict del duplicated_files[item] - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error transferring content: {e}") + def create_mutant_file(file: str, count: int, rule: str) -> None: """function to create new mutant file""" try: _, filename = os.path.split(file) # Read content from the duplicated file - with open(file, 'r', encoding="utf8") as source_file: + with open(file, "r", encoding="utf8") as source_file: content = source_file.read() # Write content to the original file - mutant_name = filename.split('.')[0] + mutant_name = filename.split(".")[0] # create folder for each contract os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True) - with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w', encoding="utf8") as mutant_file: + with open( + "mutation_campaign/" + + mutant_name + + "/" + + mutant_name + + "_" + + rule + + "_" + + str(count) + + ".sol", + "w", + encoding="utf8", + ) as mutant_file: mutant_file.write(content) # reset the file - with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: + with open(duplicated_files[file], "r", encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w', encoding="utf8") as source_file: + with open(file, "w", encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error creating mutant: {e}") + def reset_file(file: str) -> None: """function to reset the file""" try: # directory, filename = os.path.split(file) # reset the file - with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file: + with open(duplicated_files[file], "r", encoding="utf8") as duplicated_file: duplicate_content = duplicated_file.read() - with open(file, 'w', encoding="utf8") as source_file: + with open(file, "w", encoding="utf8") as source_file: source_file.write(duplicate_content) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except logger.error(f"Error resetting file: {e}") @@ -92,12 +108,13 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str sol_file_list = [] if ignore_paths is None: ignore_paths = [] + # if input is contract file if os.path.isfile(codebase): return [codebase] # if input is folder - elif os.path.isdir(codebase): + if os.path.isdir(codebase): directory = os.path.abspath(codebase) for file in os.listdir(directory): filename = os.path.join(directory, file) diff --git a/slither/tools/mutator/utils/patch.py b/slither/tools/mutator/utils/patch.py index e50f44612..54ff81e60 100644 --- a/slither/tools/mutator/utils/patch.py +++ b/slither/tools/mutator/utils/patch.py @@ -1,6 +1,7 @@ from typing import Dict, Union from collections import defaultdict + # pylint: disable=too-many-arguments def create_patch_with_line( result: Dict, @@ -9,13 +10,19 @@ def create_patch_with_line( end: int, old_str: Union[str, bytes], new_str: Union[str, bytes], - line_no: int + line_no: int, ) -> None: if isinstance(old_str, bytes): old_str = old_str.decode("utf8") if isinstance(new_str, bytes): new_str = new_str.decode("utf8") - p = {"start": start, "end": end, "old_string": old_str, "new_string": new_str, "line_number": line_no} + p = { + "start": start, + "end": end, + "old_string": old_str, + "new_string": new_str, + "line_number": line_no, + } if "patches" not in result: result["patches"] = defaultdict(list) if p not in result["patches"][file]: diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 5c75f0eb3..0e495f8ae 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -10,6 +10,7 @@ from slither.utils.colors import green, red logger = logging.getLogger("Slither-Mutate") + def compile_generated_mutant(file_path: str, mappings: str) -> bool: """ function to compile the generated mutant @@ -21,11 +22,14 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool: except: # pylint: disable=bare-except return False -def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: + +def run_test_cmd(cmd: str, test_dir: str, timeout: int) -> bool: """ function to run codebase tests returns: boolean whether the tests passed or not """ + # future purpose + _ = test_dir # add --fail-fast for foundry tests, to exit after first failure if "forge test" in cmd and "--fail-fast" not in cmd: cmd += " --fail-fast" @@ -36,49 +40,65 @@ def run_test_cmd(cmd: str, dir: str, timeout: int) -> bool: start = time.time() # starting new process - P = subprocess.Popen( - [cmd], - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=os.setsid - ) - try: - # checking whether the process is completed or not within 30 seconds(default) - while P.poll() is None and (time.time() - start) < timeout: - time.sleep(0.05) - finally: - if P.poll() is None: - logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") - # sends a SIGTERM signal to process group - bascially killing the process - os.killpg(os.getpgid(P.pid), signal.SIGTERM) - # Avoid any weird race conditions from grabbing the return code - time.sleep(0.05) - # indicates whether the command executed sucessfully or not - r = P.returncode + with subprocess.Popen( + [cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as P: + try: + # checking whether the process is completed or not within 30 seconds(default) + while P.poll() is None and (time.time() - start) < timeout: + time.sleep(0.05) + finally: + if P.poll() is None: + logger.error("HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION)") + # sends a SIGTERM signal to process group - bascially killing the process + os.killpg(os.getpgid(P.pid), signal.SIGTERM) + # Avoid any weird race conditions from grabbing the return code + time.sleep(0.05) + # indicates whether the command executed sucessfully or not + r = P.returncode # if r is 0 then it is valid mutant because tests didn't fail return r == 0 -def test_patch(file: str, patch: Dict, command: str, index: int, generator_name: str, timeout: int, mappings: str | None, verbose: bool) -> bool: # pylint: disable=too-many-arguments + +def test_patch( # pylint: disable=too-many-arguments + file: str, + patch: Dict, + command: str, + index: int, + generator_name: str, + timeout: int, + mappings: str | None, + verbose: bool, +) -> bool: """ function to verify the validity of each patch returns: valid or invalid patch """ - with open(file, 'r', encoding="utf-8") as filepath: + with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = content[:patch['start']] + patch['new_string'] + content[patch['end']:] + replaced_content = ( + content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] + ) # Write the modified content back to the file - with open(file, 'w', encoding="utf-8") as filepath: + with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) if compile_generated_mutant(file, mappings): if run_test_cmd(command, file, timeout): create_mutant_file(file, index, generator_name) - print(green(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n")) + print( + green( + f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> VALID\n" + ) + ) return True reset_file(file) if verbose: - print(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n")) + print( + red( + f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n" + ) + ) return False From c5f1537022be4f5301c3f986d6fa1a94681db1a1 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 26 Jan 2024 11:37:05 -0500 Subject: [PATCH 28/41] Formatted mutators --- slither/tools/mutator/__main__.py | 18 ++++-------------- slither/tools/mutator/mutators/LIR.py | 8 ++------ slither/tools/mutator/mutators/MVIE.py | 4 +--- slither/tools/mutator/mutators/SBR.py | 8 ++------ .../tools/mutator/mutators/abstract_mutator.py | 4 +--- .../mutator/utils/testing_generated_mutant.py | 8 ++------ 6 files changed, 12 insertions(+), 38 deletions(-) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index f0b5c88bc..5c13d7aea 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -38,9 +38,7 @@ def parse_args() -> argparse.Namespace: usage="slither-mutate --test-cmd ", ) - parser.add_argument( - "codebase", help="Codebase to analyze (.sol file, project directory, ...)" - ) + parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") parser.add_argument( "--list-mutators", @@ -60,9 +58,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--ignore-dirs", help="Directories to ignore") # time out argument - parser.add_argument( - "--timeout", help="Set timeout for test command (by default 30 seconds)" - ) + parser.add_argument("--timeout", help="Set timeout for test command (by default 30 seconds)") # output directory argument parser.add_argument( @@ -118,11 +114,7 @@ def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator] and str(c.NAME) in mutators_list ] else: - detectors = [ - c - for c in detectors_ - if inspect.isclass(c) and issubclass(c, AbstractMutator) - ] + detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator)] return detectors @@ -143,9 +135,7 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods ################################################################################### -def main() -> ( - None -): # pylint: disable=too-many-statements,too-many-branches,too-many-locals +def main() -> (None): # pylint: disable=too-many-statements,too-many-branches,too-many-locals args = parse_args() # arguments diff --git a/slither/tools/mutator/mutators/LIR.py b/slither/tools/mutator/mutators/LIR.py index 066a1fbb0..cc58cbae1 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -27,12 +27,8 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods if isinstance(variable.expression, Literal): if isinstance(variable.type, ElementaryType): - literal_replacements.append( - variable.type.min - ) # append data type min value - literal_replacements.append( - variable.type.max - ) # append data type max value + literal_replacements.append(variable.type.min) # append data type min value + literal_replacements.append(variable.type.max) # append data type max value if str(variable.type).startswith("uint"): literal_replacements.append("1") elif str(variable.type).startswith("uint"): diff --git a/slither/tools/mutator/mutators/MVIE.py b/slither/tools/mutator/mutators/MVIE.py index 4d5f6205d..ce51792ff 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -40,9 +40,7 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods for function in self.contract.functions_and_modifiers_declared: for variable in function.local_variables: - if variable.initialized and not isinstance( - variable.expression, Literal - ): + if variable.initialized and not isinstance(variable.expression, Literal): # Get the string start = variable.source_mapping.start stop = variable.expression.source_mapping.start diff --git a/slither/tools/mutator/mutators/SBR.py b/slither/tools/mutator/mutators/SBR.py index 770b57180..efbda4877 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -71,9 +71,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub( - re.compile(left_value), right_value, old_str - ) + new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line( result, self.in_file, @@ -98,9 +96,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods left_value = value.split(" ==> ", maxsplit=1)[0] right_value = value.split(" ==> ")[1] if re.search(re.compile(left_value), old_str) is not None: - new_str = re.sub( - re.compile(left_value), right_value, old_str - ) + new_str = re.sub(re.compile(left_value), right_value, old_str) create_patch_with_line( result, self.in_file, diff --git a/slither/tools/mutator/mutators/abstract_mutator.py b/slither/tools/mutator/mutators/abstract_mutator.py index 12c6b2298..375af1e6f 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -104,9 +104,7 @@ class AbstractMutator( continue self.VALID_MUTANTS_COUNT += 1 patched_txt, _ = apply_patch(original_txt, patch, 0) - diff = create_diff( - self.compilation_unit, original_txt, patched_txt, file - ) + diff = create_diff(self.compilation_unit, original_txt, patched_txt, file) if not diff: logger.info(f"Impossible to generate patch; empty {patches}") diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 0e495f8ae..6104cdd8a 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -40,9 +40,7 @@ def run_test_cmd(cmd: str, test_dir: str, timeout: int) -> bool: start = time.time() # starting new process - with subprocess.Popen( - [cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) as P: + with subprocess.Popen([cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as P: try: # checking whether the process is completed or not within 30 seconds(default) while P.poll() is None and (time.time() - start) < timeout: @@ -78,9 +76,7 @@ def test_patch( # pylint: disable=too-many-arguments with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = ( - content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] - ) + replaced_content = (content[: patch["start"]] + patch["new_string"] + content[patch["end"] :]) # Write the modified content back to the file with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) From 360509f8185d9915c98a66fefc0a9c91b30d8f84 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Fri, 26 Jan 2024 12:25:32 -0500 Subject: [PATCH 29/41] Formatted test_patch --- slither/tools/mutator/utils/testing_generated_mutant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/tools/mutator/utils/testing_generated_mutant.py b/slither/tools/mutator/utils/testing_generated_mutant.py index 6104cdd8a..4c51b7e5a 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -76,7 +76,7 @@ def test_patch( # pylint: disable=too-many-arguments with open(file, "r", encoding="utf-8") as filepath: content = filepath.read() # Perform the replacement based on the index values - replaced_content = (content[: patch["start"]] + patch["new_string"] + content[patch["end"] :]) + replaced_content = content[: patch["start"]] + patch["new_string"] + content[patch["end"] :] # Write the modified content back to the file with open(file, "w", encoding="utf-8") as filepath: filepath.write(replaced_content) From d6e40afc5ede852624693490111adbc94d13c030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:41:54 -0600 Subject: [PATCH 30/41] Bump cachix/cachix-action from 12 to 14 (#2275) Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 12 to 14. - [Release notes](https://github.com/cachix/cachix-action/releases) - [Commits](https://github.com/cachix/cachix-action/compare/v12...v14) --- updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a646bb42..9675b0d35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: - name: Set up cachix if: matrix.type == 'dapp' - uses: cachix/cachix-action@v12 + uses: cachix/cachix-action@v14 with: name: dapp From 340cbe09a6f58a8bfe48b37d93653c6a8d3293cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:42:07 -0600 Subject: [PATCH 31/41] Bump actions/upload-artifact from 3 to 4 (#2265) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e0ab4b543..dd63b5f47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: python -m pip install build python -m build - name: Upload distributions - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: slither-dists path: dist/ From b83fa21b3e1fbfbc5b92b780960ba0de084d83c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:42:16 -0600 Subject: [PATCH 32/41] Bump actions/setup-python from 4 to 5 (#2259) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/black.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/doctor.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/pip-audit.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/pylint.yml | 2 +- .github/workflows/test.yml | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 6017255b2..082333f2f 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -32,7 +32,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9675b0d35..dbde09489 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install dependencies diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e28c7136a..3d5eeed65 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' - run: pip install -e ".[doc]" diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml index 0a0eb896d..555452871 100644 --- a/.github/workflows/doctor.yml +++ b/.github/workflows/doctor.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5415b6d1b..45dc0d8c2 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/pip-audit.yml b/.github/workflows/pip-audit.yml index a98f6ab58..1c0a1d40a 100644 --- a/.github/workflows/pip-audit.yml +++ b/.github/workflows/pip-audit.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dd63b5f47..7fc17804c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7e990371f..091da2b96 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1660a25e9..68a32f80a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: "pip" @@ -102,7 +102,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 From b292175837e9da2252d45ce8dd12d5769551f28d Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:42:45 +0100 Subject: [PATCH 33/41] Detect also in modifiers (#2280) --- slither/detectors/statements/divide_before_multiply.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/statements/divide_before_multiply.py b/slither/detectors/statements/divide_before_multiply.py index 334da592c..1f6ccd87e 100644 --- a/slither/detectors/statements/divide_before_multiply.py +++ b/slither/detectors/statements/divide_before_multiply.py @@ -133,7 +133,7 @@ def detect_divide_before_multiply( results: List[Tuple[FunctionContract, List[Node]]] = [] # Loop for each function and modifier. - for function in contract.functions_declared: + for function in contract.functions_declared + contract.modifiers_declared: if not function.entry_point: continue From 28a921ec5dcdd25e21cf8b55903f23d4c097f631 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 29 Jan 2024 15:43:14 -0600 Subject: [PATCH 34/41] Fix/iterative update (#2206) * use iterative algo to prevent exceeding recursion limit * proper fixpoint --- slither/solc_parsing/declarations/function.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py index 59940ec1c..c1b94661d 100644 --- a/slither/solc_parsing/declarations/function.py +++ b/slither/solc_parsing/declarations/function.py @@ -1106,11 +1106,13 @@ class FunctionSolc(CallerContextExpression): return node def _update_reachability(self, node: Node) -> None: - if node.is_reachable: - return - node.set_is_reachable(True) - for son in node.sons: - self._update_reachability(son) + worklist = [node] + while worklist: + current = worklist.pop() + # fix point + if not current.is_reachable: + current.set_is_reachable(True) + worklist.extend(current.sons) def _parse_cfg(self, cfg: Dict) -> None: From 4454b3e34af0f21498b2a721e4698013f1100b22 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 29 Jan 2024 15:44:25 -0600 Subject: [PATCH 35/41] fix: detect selfdestruct in internal calls (#2232) * fix: detect selfdestruct in internal calls * update snapshot --- slither/detectors/functions/suicidal.py | 2 +- ...etector_Suicidal_0_7_6_suicidal_sol__0.txt | 2 ++ .../test_data/suicidal/0.7.6/suicidal.sol | 8 ++++++++ .../suicidal/0.7.6/suicidal.sol-0.7.6.zip | Bin 1478 -> 1918 bytes 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/slither/detectors/functions/suicidal.py b/slither/detectors/functions/suicidal.py index 1f8cb52f9..f0af978ec 100644 --- a/slither/detectors/functions/suicidal.py +++ b/slither/detectors/functions/suicidal.py @@ -59,7 +59,7 @@ Bob calls `kill` and destructs the contract.""" if func.visibility not in ["public", "external"]: return False - calls = [c.name for c in func.internal_calls] + calls = [c.name for c in func.all_internal_calls()] if not ("suicide(address)" in calls or "selfdestruct(address)" in calls): return False diff --git a/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt index 4a784217d..99a6a0295 100644 --- a/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt +++ b/tests/e2e/detectors/snapshots/detectors__detector_Suicidal_0_7_6_suicidal_sol__0.txt @@ -1,2 +1,4 @@ +C.i_am_a_backdoor2(address) (tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol#8-10) allows anyone to destruct the contract + C.i_am_a_backdoor() (tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol#4-6) allows anyone to destruct the contract diff --git a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol index 428c794d4..31b22d767 100644 --- a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol +++ b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol @@ -5,4 +5,12 @@ contract C{ selfdestruct(msg.sender); } + function i_am_a_backdoor2(address payable to) public{ + internal_selfdestruct(to); + } + + function internal_selfdestruct(address payable to) internal { + selfdestruct(to); + } + } diff --git a/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip b/tests/e2e/detectors/test_data/suicidal/0.7.6/suicidal.sol-0.7.6.zip index 635092d49ecdda4ce21c4da09b8ba5b061e9a72e..ecd80364b245bf08dcb196de7486ec55a28df991 100644 GIT binary patch delta 1676 zcmV;726Oqw3;qrpP)h>@KL7#%4gd+iZda}*4ZQcwI)f^lUKiXxlVyu|ACX92r)}MHOg=ndqYlx*~+h_@%>JJ&~x?e zB8z#op}$%%#&@A;RgU@Rrr1v#lR}i~4>{M%N>GzFjX(CFvpbQoa3{wcR#} zmeTK!Cxr%@F0FnHuvc362W@}%_eX;nII?MS0MrIPVJ@-KDVoo`JFmJU3?!rEflC_U zYTq@FHa5*0`)GgRY^d0>57pCPCbmYO=&3YN#{$u){#$h$nCE~Ex}qrSA4KJm83p|* z?YKSyiz5ZE&JN6-+Z4lbKo7{djAz(EF5{FYoqb`NvO395Czu*KYH&W{(m4>6YNCgt@7U{4r6ocI<=z0(!Jwv zAloCKXWo9%31PfDKS+PPb0Bq%w1dWAmEAUXI&+G$Y}4ceqZwuYZPo6`8tv@%FB?tk z0C{C8PtEf}b8%Qs1HCnqDsx+cR^FY!1`MRN+Yitp0`W;FMIJbQVZy34=7aBUqdwZE zCOMLN#>{?>B-81ABfdc1InZUQVCxOoS;#&9FI~LKwB@mR>1Ut%hnnM@@WQZhANXO!Z#VV3fqjNF9$7dl<5jOSPB=mv zww^el)k9`zOYo>EhjRKqZ1*@o`xW2kG@@&qD*z(lmy2HeJD+tC{&Z3XdCYxH)D9}a zma2&s4;qRD1O0yqs~Ami+MTlW!=7Aye(CJQopm>-PHiT?Dmva5JoJu3rwOb}f$gjO z^8kNPjvM|qH6_8@GP3u77;s)$41+1;hY{`YfnK^3(+YRpE+U;JehNr;$wpqN<;!>1 z{Of*a{N{rG=y1pD*cOo;afKQI#EXz8|jQp2OE zSHD>T2&j|kV11gZ_Wv+RUb3v3aw6!+D?TWjQ>oNs3d@#}S=dIfaFkkjcIit|Az0w{ z4$kXjZ*x!5wvQimQyDwx6NaeX)^RIV7g^QWIX#jC5bm7q*5ageZ_j4F%VapRVeK=Gknl<#hRfyqrgfgRy$RzKe86-%U4 zKy^7)>xTfY)(pVQ_zNx!$Y588tV~wNzxwG$mOY2%(-=c|I zxpnD7zk?t4cZi1@M!fHVt^}TP5OT-p1`QAYXw)@>lFXwe->TG@xKlY z@P)4FOh#yS8QPkpxi=kr)lG0?__jg>wE@izk8_W@x$0toGF6VGtFOYJ{g*@1@YZhi z#di8L!XvPA{k!ywLY}fKV4c0$k8<}(|ujhO?32XSyhJyYe*g~Vd55(xxq$f z9YlZZ37juY^iZ){)SJj<%dn6TV8b6+ml(=G+nI({{oi952&bOj#9$J;PR#gd6a$M|R@vuPXR4BmGEt$wRfqf@Z$$b-0Vz zP#~f#X9Boss)|_)#yT6vO_m2QS~Slkj>LZx8KYV2w9>chDi))(J+cd@Be)3qLdAi~ zrS3FP9~}Likz3-VzDjnqykc zxTb9pn4*WqHkQj4w6mjyJwccq(C*0PU5fEUwXo9y1N{FlVI$K2@Q}3&apO_1Du#c{ zv!KpH^s^dzo{=1`;)nvq138^zRu75s1aOuA4cBQ%f4$0FvQqX5e)~z$NJe;bkZ6WK z5s<`mD7wa=S$Rk4b*lLRX#=U*2VOmr%R>qZ8V!rZ@KL7#%4gfQacUDm|1vn%H0012mkr>Z^t>?SvX?p7Dh;w36 z@yXD87z=#tMF0So=QQCRp0K@Q(TGGPCl1U{6^Nxy9(#xgVo4QZ4n`GPH2%(uKMNZ< zX>M?Bb_4*Kd%)Ml6ruYCkOuUV!(@e3`sp=&qLytSTIn3hVM7t~EuR7jZy${w0|l7qSlpoJP_QVGsOIqM2p;XduV`05(PhsER9^^PB1$!1IyT zmeWoQZ(c~q1eI`)jITj$*6uom#c@mxymrdtLSm0{{a0Rp$~rX^eJMc2^J(V(1-HSaiNhZ=D?Q^z-$6h$Rg(I%(dC8~e)F+pD zPf5vrsxF(QL=9be2%7JX( zt_wQh8yVd1FV3A2c)@5>1VRpy0pV9x=y9Bz$8${$P|x^XADgfSn-ayRAz9UNz_s1AhhwVqQ;-7KLj(C+GJwz0L6@l<3dO>dei$PFH7PrPFZm9WW?xHTSBNmbV z;;D-)9{8ml4Ymd Date: Tue, 30 Jan 2024 08:36:21 -0600 Subject: [PATCH 36/41] Bump actions/deploy-pages from 3 to 4 (#2285) Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 3 to 4. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3d5eeed65..4292d5f44 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,4 +43,4 @@ jobs: path: './html/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v3 + uses: actions/deploy-pages@v4 From 1bd279a0179fe1ec52f98f02e736cce3ed39c53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:36:32 -0600 Subject: [PATCH 37/41] Bump cachix/install-nix-action from 24 to 25 (#2286) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 24 to 25. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v24...v25) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbde09489..7972c96e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v24 + uses: cachix/install-nix-action@v25 - name: Set up cachix if: matrix.type == 'dapp' From 1b0d63b428ac7c66c70ae359fdfa220b445166a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:46:35 -0600 Subject: [PATCH 38/41] Bump sigstore/gh-action-sigstore-python from 2.1.0 to 2.1.1 (#2293) Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases) - [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: sigstore/gh-action-sigstore-python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7fc17804c..72b002d1e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,7 +47,7 @@ jobs: uses: pypa/gh-action-pypi-publish@v1.8.11 - name: sign - uses: sigstore/gh-action-sigstore-python@v2.1.0 + uses: sigstore/gh-action-sigstore-python@v2.1.1 with: inputs: ./dist/*.tar.gz ./dist/*.whl release-signing-artifacts: true From 9bf4d07b04480da90c1f2f023a0dd7d1e8dba21f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:50:04 -0600 Subject: [PATCH 39/41] Bump actions/upload-pages-artifact from 2 to 3 (#2294) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4292d5f44..0942afb6d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,7 @@ jobs: - run: pip install -e ".[doc]" - run: pdoc -o html/ slither '!slither.tools' #TODO fix import errors on pdoc run - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: # Upload the doc path: './html/' From 6620bc926e7d8435256bf9bf0c774c22e0302241 Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:54:04 +0100 Subject: [PATCH 40/41] Fix using for when used with "this" (#2224) --- slither/slithir/convert.py | 23 ++++++++++++++++++ tests/e2e/solc_parsing/test_ast_parsing.py | 1 + ...g-for-this-contract.sol-0.8.15-compact.zip | Bin 0 -> 2745 bytes ...-for-this-contract.sol-0.8.15-compact.json | 8 ++++++ .../test_data/using-for-this-contract.sol | 13 ++++++++++ 5 files changed, 45 insertions(+) create mode 100644 tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip create mode 100644 tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json create mode 100644 tests/e2e/solc_parsing/test_data/using-for-this-contract.sol diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 4411e3505..170df8cba 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -630,6 +630,17 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo if new_ir: return new_ir + # convert library function when used with "this" + if ( + isinstance(t, ElementaryType) + and t.name == "address" + and ir.destination.name == "this" + and UserDefinedType(node_function.contract) in using_for + ): + new_ir = convert_to_library_or_top_level(ir, node, using_for) + if new_ir: + return new_ir + if isinstance(t, UserDefinedType): # UserdefinedType t_type = t.type @@ -1564,6 +1575,18 @@ def convert_to_library_or_top_level( if new_ir: return new_ir + if ( + isinstance(t, ElementaryType) + and t.name == "address" + and ir.destination.name == "this" + and UserDefinedType(node.function.contract) in using_for + ): + new_ir = look_for_library_or_top_level( + contract, ir, using_for, UserDefinedType(node.function.contract) + ) + if new_ir: + return new_ir + return None diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index bc57dc51b..e233fa993 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -448,6 +448,7 @@ ALL_TESTS = [ Test("using-for-functions-list-3-0.8.0.sol", ["0.8.15"]), Test("using-for-functions-list-4-0.8.0.sol", ["0.8.15"]), Test("using-for-global-0.8.0.sol", ["0.8.15"]), + Test("using-for-this-contract.sol", ["0.8.15"]), Test("library_event-0.8.16.sol", ["0.8.16"]), Test("top-level-struct-0.8.0.sol", ["0.8.0"]), Test("yul-top-level-0.8.0.sol", ["0.8.0"]), diff --git a/tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip b/tests/e2e/solc_parsing/test_data/compile/using-for-this-contract.sol-0.8.15-compact.zip new file mode 100644 index 0000000000000000000000000000000000000000..6950666a479e8b3c138633675a65c8f101cd0069 GIT binary patch literal 2745 zcmb7`Ra_H{0>wv4j8tl%#D>&IGhq@VB&0q{3X~}f;37?DLD`X zCEcWSz(5oR1K|_7e)s)8+;bkz?>wB>^ZVOaFfqdci~w#x!6W3&+|g0`F*^XDt^fci z0|0TT8&6^^j_E1P_HTO^@-K`SBGL zreiC6zf;ESp?fC6AZ)yvbyVRpVL-@5oWJyav~$!x|Wv9rstTQzZ!Z=GAeu?c2N3C?7je*LzOe+DCpF$nQ=c;RO}UGlN9feOu`*7b@&sTWic?pHGtoefC?e`Y>Z|>-$sz zsaE@QTH0feEisgIDn@N1ionW}ESn+a7**#f@tMUFdp>FJaI=SHq95!XT%*6Gk)@aE zXH4sl(YtXXk|nIgqU?Y5!e^f+Vb65ne_sTG$g02H$<~q(q`@V85XM>vRAfXg6GO5? zETh;YHTdEl`}l8X`#;&S?D?BU)vtk7!qj(&E#}y5{&f2bX7xnn?XPkl$EuRwNDF%& ztEq_XN}r9PM(eBII-j~)PPYl6n-RYpOe!Zk{imgpi1lP@yr3M?7|=VG|HkJY1kr`v zAwPB4WZvFdtRJ55v@xKMdHjHHJrLLoO!qCUbY7MzN5AYIqP%UHn~FKssQe6n)Qank zVpOP)Rm?OQP%CvlRu|#cR4|1vH z8brmkO@oJ{ptfTLiZi?`TfOv3CZV~;id$vU)MXCgeutwU_7z;=wyCig!<(vl_*AMF zKOpr`HiVbj31zdarEz4VLypIycD&M1NqN%vJI_v2#e4vERs&QHyDY6vUlF{H;PUvdT z!~jGsUL{Zc}!-VVAS)cijcXdIX2o zXmfF@Y7-s24!_9^ix>;vd)@(Xmw8Jk^k}k{!{BiPY_Y|DgC0 z1K)G0G3dWjR3Gn)KIm7SumJ_^P!+cjgHEFx!d)i<(Ebp%x_8&&mxV-(f>xV3D9K0s zUrMHROynM%+$1MXjkr9%?h)kH-FTC8F?#;ZLWsRMcg9MwQQfb7tTcGQnJE_B0P75rFeB$4JES300yu!=luY8|OMWHbk2hg zN*nW%T3TfWvtfz#Yy6_|GSlk4roI(3zskZvxZ?vafS(|r!2I-$_Ew`O{lP0&Umx;v z3mIv%9}!Hsqh;36&f`MrDn2nU4Ood)D1(1O&H~mzpYPMCM&DqC`o8%9rOwC7Y0o1n zBC}nV7Vq0az2QOi+TE?)nkMR{sC?*jKk$@r*XGMm+Kdp}{A27L%;!+!tPDw?S9@}l z#vaV4BD(*d*EAQ)SH{K;3VW6o7&r>xA7-KGLVm0%F7XV~;)asc=#S=hOchO)a%3k~ zvDgsLQG;c{HhdyZ>tJ0myB~yj&I_-dJC8o8A$_)Q5_8lx@g=sTZ1t%<5*42#rd5+I zBQ>yMcgo?jIbTUh4gRkV48@@KqU;sJ2G?ct6%}$dl}ErgmeGW*D>X|yB=oM>;*R-wY|vW zdo9wk8^+0i9B*mxhUqiih<``0va_VFo+&a~pWVof2>9YQAvPZ>*Q?R~{WAT~DIort zgVb@I*lMt96F+yTp!nS^28#7$6#v{q7($|(x7UKC0Cf8+Pe*Cl{;lVru%_olp%+lw zrhLzL)jS-paQFy)m_a22^W?QG>=p-+dKJMiBlYp%MVG@7M2au%7tCZMJrU!0+Z0XRN+-p=C~*@|Oh}ebS1;B*_61W`+1CgaN=mAsskJ@7%J!z& z@zEpRd+DZ3RMtQx_mkxYfy^Aq!Tn|lY5BD4{T7&7K8y9Uf*WFcK`>_Wn*5WCU7_HM zgsH!#ZZ1t?e*$v`J0;aa{&~28D^;0|)UAuJ*>G_)zxo@+vhZ!|lFEdv=B+yWxa(K7 z8@TpZ?~Uh^G2>19B~bflR%9Q6e%{rQhE&9{?M6t4e~hhBHrh4^A7%w&Z|-SQf{K<; zno5#&>Ew*tBgHuc_VuDB%oYX7ALKlF&nYuO%{VNjmIIFWvRa66p9QBmo0?NRJd77q zJf^z-Qq)-*AZ3nf?C?2MP>8Q1ZtZPa`Dr;UT^z$G;(j({A3s=Zt}~+?fy$F7H87K?NRh@W?eEF!${F?xds%4llo4VTj%c*SVKq)kAwh#06X2wO$J3_uPc{Ou7m_ zrSQyLZxH7Hp{f|82?xv{%!vOEW|0Q literal 0 HcmV?d00001 diff --git a/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json b/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json new file mode 100644 index 000000000..43eca2c9a --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/using-for-this-contract.sol-0.8.15-compact.json @@ -0,0 +1,8 @@ +{ + "Lib": { + "f(Hello)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n" + }, + "Hello": { + "test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol b/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol new file mode 100644 index 000000000..33bbc74cd --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/using-for-this-contract.sol @@ -0,0 +1,13 @@ +library Lib { + function f(Hello h) external { + + } +} +contract Hello { + using Lib for Hello; + + function test() external { + this.f(); + } +} + From e876d61fd51fac747fd22f906026037161f7d24f Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Thu, 8 Feb 2024 13:27:50 -0800 Subject: [PATCH 41/41] fix: broken doc links (#2299) * fix: broken doc links * style: black --- README.md | 6 +++--- slither/detectors/assembly/incorrect_return.py | 4 +++- slither/detectors/assembly/return_instead_of_leave.py | 2 +- tests/e2e/compilation/test_resolution.py | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 37f678d7a..769ac58aa 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Slither - Read the Docs](https://img.shields.io/badge/Slither-Read_the_Docs-2ea44f)](https://crytic.github.io/slither/slither.html) [![Slither - Wiki](https://img.shields.io/badge/Slither-Wiki-2ea44f)](https://github.com/crytic/slither/wiki/SlithIR) -> Join the Empire Hacking Slack +> Join the Empire Hacking Slack > > [![Slack Status](https://slack.empirehacking.nyc/badge.svg)](https://slack.empirehacking.nyc/) > > - Discussions and Support @@ -131,10 +131,10 @@ Num | Detector | What it Detects | Impact | Confidence 20 | `controlled-delegatecall` | [Controlled delegatecall destination](https://github.com/crytic/slither/wiki/Detector-Documentation#controlled-delegatecall) | High | Medium 21 | `delegatecall-loop` | [Payable functions using `delegatecall` inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#payable-functions-using-delegatecall-inside-a-loop) | High | Medium 22 | `incorrect-exp` | [Incorrect exponentiation](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation) | High | Medium -23 | `incorrect-return` | [If a `return` is incorrectly used in assembly mode.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium +23 | `incorrect-return` | [If a `return` is incorrectly used in assembly mode.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-return-in-assembly) | High | Medium 24 | `msg-value-loop` | [msg.value inside a loop](https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop) | High | Medium 25 | `reentrancy-eth` | [Reentrancy vulnerabilities (theft of ethers)](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities) | High | Medium -26 | `return-leave` | [If a `return` is used instead of a `leave`.](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return) | High | Medium +26 | `return-leave` | [If a `return` is used instead of a `leave`.](https://github.com/crytic/slither/wiki/Detector-Documentation#return-instead-of-leave-in-assembly) | High | Medium 27 | `storage-array` | [Signed storage integer array compiler bug](https://github.com/crytic/slither/wiki/Detector-Documentation#storage-signed-integer-array) | High | Medium 28 | `unchecked-transfer` | [Unchecked tokens transfer](https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer) | High | Medium 29 | `weak-prng` | [Weak PRNG](https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG) | High | Medium diff --git a/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py index f5f0a98d9..bd5a6d844 100644 --- a/slither/detectors/assembly/incorrect_return.py +++ b/slither/detectors/assembly/incorrect_return.py @@ -39,7 +39,9 @@ class IncorrectReturn(AbstractDetector): IMPACT = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.MEDIUM - WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + WIKI = ( + "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-return-in-assembly" + ) WIKI_TITLE = "Incorrect return in assembly" WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." diff --git a/slither/detectors/assembly/return_instead_of_leave.py b/slither/detectors/assembly/return_instead_of_leave.py index a1591d834..a1ad9c87e 100644 --- a/slither/detectors/assembly/return_instead_of_leave.py +++ b/slither/detectors/assembly/return_instead_of_leave.py @@ -20,7 +20,7 @@ class ReturnInsteadOfLeave(AbstractDetector): IMPACT = DetectorClassification.HIGH CONFIDENCE = DetectorClassification.MEDIUM - WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-instead-of-leave-in-assembly" WIKI_TITLE = "Return instead of leave in assembly" WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." diff --git a/tests/e2e/compilation/test_resolution.py b/tests/e2e/compilation/test_resolution.py index af7cbe2c7..c3290624b 100644 --- a/tests/e2e/compilation/test_resolution.py +++ b/tests/e2e/compilation/test_resolution.py @@ -57,6 +57,6 @@ def test_contract_function_parameter(solc_binary_path) -> None: function = contract.functions[0] parameters = function.parameters - assert (parameters[0].name == 'param1') - assert (parameters[1].name == '') - assert (parameters[2].name == 'param3') + assert parameters[0].name == "param1" + assert parameters[1].name == "" + assert parameters[2].name == "param3"