From a0365982add762e2bb4abb80cdea9b272bcddd61 Mon Sep 17 00:00:00 2001 From: Vishnuram Rajkumar Date: Tue, 2 Jan 2024 09:20:59 -0500 Subject: [PATCH 01/16] 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 84286ce66c..fde87e5e2c 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,7 +2,8 @@ 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.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 @@ ################################################################################### ################################################################################### - 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 @@ def __call__( 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 405888f8bf..370f419c18 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 a16a8252e2..9dccc9f131 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 d4a7c54868..9e7b143ae8 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 169d8725e4..ab295e2958 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -4,9 +4,10 @@ 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 @@ def __init__( ) @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 @@ def mutate(self) -> None: 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 feb479c5c8..80d610a69a 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 0000000000..800232a5a5 --- /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 d773ea7844..03ccadec77 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 0000000000..82d6c11c98 --- /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 0000000000..1c7de9acf8 --- /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 02/16] 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 fde87e5e2c..9cb1a9f8da 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 370f419c18..b1c55d184f 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 9dccc9f131..1c99b24eef 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 9e7b143ae8..bb1c4cabe0 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 ab295e2958..a79f459061 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 @@ def _mutate(self, test_cmd: str, test_dir: str) -> Dict: """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 @@ def mutate(self, testing_command: str, testing_directory: str) -> int: # 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 5508fb68e5..5a9465c860 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 800232a5a5..c041f76d6e 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 82d6c11c98..4e3f91454b 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 1c7de9acf8..61b9db2141 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 03/16] 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 9cb1a9f8da..201b39acdd 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,6 +4,7 @@ import sys from typing import Type, List, Any, Dict, Tuple import os +import shutil from crytic_compile import cryticparser @@ -12,12 +13,12 @@ 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 @@ def __call__( ################################################################################### ################################################################################### - 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 0000000000..382a08aeec --- /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 b1c55d184f..d76369d4d4 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 1c99b24eef..15b2a20c3a 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 bb1c4cabe0..5e7c0a6e1b 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,41 +4,32 @@ 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 0000000000..9a8942e196 --- /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 0000000000..ac35d9540e --- /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 0000000000..6d5862e765 --- /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 a79f459061..e58b705a19 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 @@ 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)]: + 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 @@ def mutate(self, testing_command: str, testing_directory: str, contract_name: st 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 5a9465c860..4b0c3b1ada 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 c041f76d6e..70f7deb116 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -4,9 +4,10 @@ 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 03ccadec77..6c0ba65117 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.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 61b9db2141..7ab4065191 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -2,8 +2,13 @@ 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 04/16] 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 201b39acdd..aba4273555 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 @@ def __call__( 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 0000000000..f248d3c771 --- /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 0000000000..8c70756a24 --- /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 0000000000..7878a20eaa --- /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 382a08aeec..b00b130001 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 @@ def _mutate(self) -> Dict: # 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 d76369d4d4..1cc0fdb6db 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -2,6 +2,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 +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 15b2a20c3a..13a4e6d1a6 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 5e7c0a6e1b..e705ff94af 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 0000000000..1e20674251 --- /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 9a8942e196..6a2ef6426f 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 @@ def _mutate(self) -> Dict: # 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 ac35d9540e..6797c0c35f 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -3,6 +3,7 @@ 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 @@ "(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 6d5862e765..60e8c22c35 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 e58b705a19..ead78ac979 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 @@ def __init__( 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 @@ def mutate(self, testing_command: str, testing_directory: str, contract_name: st # 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 4b0c3b1ada..3f4e590436 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 6c0ba65117..0000000000 --- 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 4e3f91454b..0000000000 --- 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 7ab4065191..61a688f7c5 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 05/16] 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} (81%) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index aba4273555..c41a82e35f 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -2,12 +2,10 @@ 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 @@ def __call__( 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 f248d3c771..39e3a6a20e 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 @@ 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 @@ def _mutate(self) -> Dict: 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 8c70756a24..04ad8f8b03 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 @@ 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 @@ def _mutate(self) -> Dict: 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 7878a20eaa..de9ad287e1 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 0000000000..5709346b15 --- /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 0000000000..99228913aa --- /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 b00b130001..fa66c7bca2 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 @@ 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 @@ def _mutate(self) -> Dict: 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 1cc0fdb6db..f5bc52b16e 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 13a4e6d1a6..3c334e9e68 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 e705ff94af..7f75b1dcf1 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -2,13 +2,12 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 1e20674251..c6bacf485d 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 @@ def _mutate(self) -> Dict: 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 0000000000..96aebc68f9 --- /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 6a2ef6426f..e1c78c1eb0 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 @@ 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 @@ def _mutate(self) -> Dict: 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 6797c0c35f..91c05b884e 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 @@ "(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 81% rename from slither/tools/mutator/mutators/UOI.py rename to slither/tools/mutator/mutators/UOR.py index 60e8c22c35..671ee676d5 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 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 ead78ac979..5223e2d81f 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( # 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 @@ def mutate(self) -> Tuple[int, int]: 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 3f4e590436..9aa81a0fc4 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 80d610a69a..042b4fff70 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 70f7deb116..d61bba58af 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 61a688f7c5..6c6e80a556 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -7,38 +7,44 @@ 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 06/16] 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 c41a82e35f..8cbb103adc 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 5223e2d81f..bd40ad6acd 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -97,7 +97,7 @@ def get_exist_flag(self) -> bool: 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 6c6e80a556..65cf78c276 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 07/16] 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} (67%) create mode 100644 slither/tools/mutator/utils/patch.py diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 8cbb103adc..66c0941a6d 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 @@ def __call__( 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 39e3a6a20e..61eed67b1f 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 @@ def _mutate(self) -> Dict: 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 04ad8f8b03..e6fa414378 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 @@ def _mutate(self) -> Dict: 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 de9ad287e1..5ca46950fe 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 @@ def _mutate(self) -> Dict: 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 0000000000..1935c43e60 --- /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 5709346b15..3758306f3f 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 @@ def _mutate(self) -> Dict: 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 99228913aa..c9966f1bec 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -2,7 +2,7 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 fa66c7bca2..e6903a01e9 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 @@ def _mutate(self) -> Dict: # 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 f5bc52b16e..0217324e09 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 @@ def _mutate(self) -> Dict: # 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 3c334e9e68..c81182c432 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -2,7 +2,7 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 7f75b1dcf1..890c55b1cc 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 c6bacf485d..9bb6a16001 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 @@ def _mutate(self) -> Dict: 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 e1c78c1eb0..2a38ce323b 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 @@ def _mutate(self) -> Dict: # 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 67% rename from slither/tools/mutator/mutators/RCR.py rename to slither/tools/mutator/mutators/RR.py index 96aebc68f9..95cbf617a7 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 @@ def _mutate(self) -> Dict: 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 91c05b884e..9e9afc1e5f 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 671ee676d5..aac4b02a64 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 @@ def _mutate(self) -> Dict: 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 9aa81a0fc4..21925317dd 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.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 d61bba58af..01a142c8d0 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 0000000000..39c77f6731 --- /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 65cf78c276..8525e28084 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -4,7 +4,7 @@ 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 08/16] 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 66c0941a6d..e576970f7f 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 3758306f3f..a5d7f5f7fc 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -3,8 +3,6 @@ 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 bd40ad6acd..edd5843860 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -7,6 +7,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 8525e28084..2013858df2 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.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 09/16] 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 e576970f7f..80d025c970 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 61eed67b1f..11593ee372 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 @@ def _mutate(self) -> Dict: 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 e6fa414378..29a6dc6726 100644 --- a/slither/tools/mutator/mutators/ASOR.py +++ b/slither/tools/mutator/mutators/ASOR.py @@ -42,7 +42,8 @@ def _mutate(self) -> Dict: 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 5ca46950fe..6a5552a5f5 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -31,7 +31,8 @@ def _mutate(self) -> Dict: 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 1935c43e60..79a63a88fb 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -20,8 +20,9 @@ def _mutate(self) -> Dict: 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 a5d7f5f7fc..84bfebe227 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -24,11 +24,12 @@ def _mutate(self) -> Dict: 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 c9966f1bec..e8217e9355 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -36,20 +36,20 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 e6903a01e9..e14c85708f 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -29,8 +29,8 @@ def _mutate(self) -> Dict: 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 0217324e09..421f94a5e5 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -20,16 +20,14 @@ def _mutate(self) -> Dict: 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 c81182c432..4ae387d294 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -25,18 +25,18 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 890c55b1cc..68bc2f2d83 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -27,15 +27,16 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 9bb6a16001..4c1e91b324 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -20,8 +20,8 @@ def _mutate(self) -> Dict: 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 2a38ce323b..da75a592ab 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -33,9 +33,9 @@ def _mutate(self) -> Dict: 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 95cbf617a7..3aab255eca 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -20,9 +20,10 @@ def _mutate(self) -> Dict: 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 9e9afc1e5f..f6adfff845 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -56,18 +56,19 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 aac4b02a64..fa00b52ae7 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -29,23 +29,24 @@ def _mutate(self) -> Dict: 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 edd5843860..f11d69db1d 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 @@ def __init__( 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 @@ def __init__( 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 @@ def _mutate(self) -> Dict: """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 @@ def mutate(self) -> Tuple[int, int]: 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 @@ def mutate(self) -> Tuple[int, int]: # 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 2013858df2..8525e28084 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.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 10/16] 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 0000000000..11b0b7eb51 --- /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 80d025c970..b375e9558a 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 f11d69db1d..558a514513 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -3,7 +3,6 @@ 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 @@ def mutate(self) -> Tuple[int, int, List[int]]: 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 01a142c8d0..ffebee9fc9 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 11/16] 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 11b0b7eb51..c615b0b795 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 b375e9558a..7bd4f8d791 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -26,7 +26,7 @@ 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 12/16] 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 c615b0b795..30b83ac460 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 7bd4f8d791..13e78a30ec 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 11593ee372..0c7f906b38 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 @@ 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 29a6dc6726..551cf8c644 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 @@ 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 6a5552a5f5..b6a9ae749c 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 @@ 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 79a63a88fb..5c34416e9f 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 84bfebe227..847466611c 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 @@ 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 e8217e9355..29efe6502c 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 @@ 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 e14c85708f..a8fff1c31f 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 @@ 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 421f94a5e5..5920143499 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 4ae387d294..b562ba9b0b 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 68bc2f2d83..dcb5be2592 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 4c1e91b324..b482ab5661 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 da75a592ab..826b15a4ad 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 @@ 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 3aab255eca..5afe188928 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 f6adfff845..0e94329089 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 @@ 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 fa00b52ae7..cf62c21bd1 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 @@ 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 558a514513..ea331e7c5c 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 @@ 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 @@ def __init__( 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 @@ def mutate(self) -> Tuple[int, int, List[int]]: # 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 042b4fff70..358586688c 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 13/16] 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 30b83ac460..8af977b082 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 13e78a30ec..56582d6f54 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -4,14 +4,14 @@ 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 @@ ################################################################################### 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 @@ def __call__( output_mutators(checks) parser.exit() - # endregion ################################################################################### ################################################################################### @@ -130,46 +133,46 @@ def __call__( ################################################################################### ################################################################################### -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 0c7f906b38..00506a1bbb 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 @@ def _mutate(self) -> Dict: # 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 551cf8c644..0e7452594f 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: # 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 b6a9ae749c..9f8ca2a118 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 @@ def _mutate(self) -> Dict: # 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 5c34416e9f..2960282362 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 847466611c..4560db4da2 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 @@ def _mutate(self) -> Dict: 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 29efe6502c..f9ef874cd9 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: new_str, line_no[0] ) - return result diff --git a/slither/tools/mutator/mutators/LOR.py b/slither/tools/mutator/mutators/LOR.py index a8fff1c31f..21837d81e7 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 @@ def _mutate(self) -> Dict: # 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 5920143499..a10ce1f1d7 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 @@ def _mutate(self) -> Dict: 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 b562ba9b0b..678b3b7475 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -56,5 +56,4 @@ def _mutate(self) -> Dict: 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 dcb5be2592..c82f132d74 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -55,5 +55,4 @@ def _mutate(self) -> Dict: 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 b482ab5661..20447290aa 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 @@ def _mutate(self) -> Dict: 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 826b15a4ad..d59e7575a9 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 @@ def _mutate(self) -> Dict: 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 5afe188928..6bfd644e05 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -12,20 +12,14 @@ def _mutate(self) -> 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: + 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 0e94329089..bbdc631943 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 @@ "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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 cf62c21bd1..b7e8c81569 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 @@ def _mutate(self) -> Dict: 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 ea331e7c5c..c2f15cd748 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -11,24 +11,24 @@ 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 @@ def __init__( 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 @@ 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}") + 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 @@ def mutate(self) -> Tuple[int, int, List[int]]: # 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 21925317dd..852a8efc34 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.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.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 ffebee9fc9..d4474c3101 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,47 +6,49 @@ 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 39c77f6731..e50f44612b 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 8525e28084..5c75f0eb31 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 14/16] 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 56582d6f54..f0b5c88bce 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -11,7 +11,11 @@ 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 @@ ################################################################################### ################################################################################### + 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 @@ def __call__( output_mutators(checks) parser.exit() + # endregion ################################################################################### ################################################################################### @@ -133,7 +142,10 @@ def __call__( ################################################################################### ################################################################################### -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 00506a1bbb..0bf0fb2a29 100644 --- a/slither/tools/mutator/mutators/AOR.py +++ b/slither/tools/mutator/mutators/AOR.py @@ -9,20 +9,23 @@ 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 @@ def _mutate(self) -> Dict: 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 0e7452594f..2ff403b386 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 @@ 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 @@ def _mutate(self) -> Dict: 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 9f8ca2a118..a8720a4b63 100644 --- a/slither/tools/mutator/mutators/BOR.py +++ b/slither/tools/mutator/mutators/BOR.py @@ -8,9 +8,10 @@ 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 @@ def _mutate(self) -> Dict: 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 2960282362..ebf93bf18a 100644 --- a/slither/tools/mutator/mutators/CR.py +++ b/slither/tools/mutator/mutators/CR.py @@ -6,14 +6,20 @@ 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 @@ def _mutate(self) -> Dict: 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 4560db4da2..028c1916cd 100644 --- a/slither/tools/mutator/mutators/FHR.py +++ b/slither/tools/mutator/mutators/FHR.py @@ -7,27 +7,36 @@ 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 f9ef874cd9..066a1fbb0e 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -7,16 +7,19 @@ 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 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches 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 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches 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 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches 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 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches 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 21837d81e7..2d1535b1aa 100644 --- a/slither/tools/mutator/mutators/LOR.py +++ b/slither/tools/mutator/mutators/LOR.py @@ -8,6 +8,7 @@ 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 @@ def _mutate(self) -> Dict: 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 a10ce1f1d7..f29569f63e 100644 --- a/slither/tools/mutator/mutators/MIA.py +++ b/slither/tools/mutator/mutators/MIA.py @@ -4,6 +4,7 @@ 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 @@ def _mutate(self) -> Dict: # 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 678b3b7475..4d5f6205d5 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -4,6 +4,7 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 c82f132d74..f9e51c5533 100644 --- a/slither/tools/mutator/mutators/MVIV.py +++ b/slither/tools/mutator/mutators/MVIV.py @@ -4,6 +4,7 @@ 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 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 20447290aa..9682f10caf 100644 --- a/slither/tools/mutator/mutators/MWA.py +++ b/slither/tools/mutator/mutators/MWA.py @@ -4,6 +4,7 @@ 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 @@ def _mutate(self) -> Dict: 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 d59e7575a9..9daae0663f 100644 --- a/slither/tools/mutator/mutators/ROR.py +++ b/slither/tools/mutator/mutators/ROR.py @@ -12,6 +12,7 @@ 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 @@ def _mutate(self) -> Dict: 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 6bfd644e05..e285d7a3f4 100644 --- a/slither/tools/mutator/mutators/RR.py +++ b/slither/tools/mutator/mutators/RR.py @@ -3,23 +3,36 @@ 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 bbdc631943..770b57180d 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -6,29 +6,29 @@ 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 @@ "(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 b7e8c81569..f427c2fbf6 100644 --- a/slither/tools/mutator/mutators/UOR.py +++ b/slither/tools/mutator/mutators/UOR.py @@ -8,9 +8,10 @@ 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 c2f15cd748..12c6b22983 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -9,17 +9,22 @@ 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 @@ def __init__( # pylint: disable=too-many-arguments 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 @@ def mutate(self) -> Tuple[int, int, List[int]]: (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 @@ def mutate(self) -> Tuple[int, int, List[int]]: 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 852a8efc34..b02a2cc9b9 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 358586688c..79d7050972 100644 --- a/slither/tools/mutator/utils/command_line.py +++ b/slither/tools/mutator/utils/command_line.py @@ -2,6 +2,7 @@ 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 d4474c3101..ddb3efb50a 100644 --- a/slither/tools/mutator/utils/file_handling.py +++ b/slither/tools/mutator/utils/file_handling.py @@ -6,6 +6,7 @@ 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 e50f44612b..54ff81e60b 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 5c75f0eb31..0e495f8ae6 100644 --- a/slither/tools/mutator/utils/testing_generated_mutant.py +++ b/slither/tools/mutator/utils/testing_generated_mutant.py @@ -10,6 +10,7 @@ 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 15/16] 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 f0b5c88bce..5c13d7aeae 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 @@ def __call__( ################################################################################### -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 066a1fbb0e..cc58cbae16 100644 --- a/slither/tools/mutator/mutators/LIR.py +++ b/slither/tools/mutator/mutators/LIR.py @@ -27,12 +27,8 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches 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 4d5f6205d5..ce51792ffc 100644 --- a/slither/tools/mutator/mutators/MVIE.py +++ b/slither/tools/mutator/mutators/MVIE.py @@ -40,9 +40,7 @@ def _mutate(self) -> Dict: 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 770b57180d..efbda48774 100644 --- a/slither/tools/mutator/mutators/SBR.py +++ b/slither/tools/mutator/mutators/SBR.py @@ -71,9 +71,7 @@ def _mutate(self) -> Dict: 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 @@ def _mutate(self) -> Dict: 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 12c6b22983..375af1e6fd 100644 --- a/slither/tools/mutator/mutators/abstract_mutator.py +++ b/slither/tools/mutator/mutators/abstract_mutator.py @@ -104,9 +104,7 @@ def mutate(self) -> Tuple[int, int, List[int]]: 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 0e495f8ae6..6104cdd8a9 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 16/16] 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 6104cdd8a9..4c51b7e5af 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)