updated mutator

pull/2278/head
Vishnuram Rajkumar 11 months ago
parent 40536d8e88
commit a0365982ad
  1. 81
      slither/tools/mutator/__main__.py
  2. 73
      slither/tools/mutator/mutators/MIA.py
  3. 45
      slither/tools/mutator/mutators/MVIE.py
  4. 45
      slither/tools/mutator/mutators/MVIV.py
  5. 19
      slither/tools/mutator/mutators/abstract_mutator.py
  6. 2
      slither/tools/mutator/utils/command_line.py
  7. 77
      slither/tools/mutator/utils/file_handling.py
  8. 30
      slither/tools/mutator/utils/generic_patching.py
  9. 43
      slither/tools/mutator/utils/replace_conditions.py
  10. 31
      slither/tools/mutator/utils/testing_generated_mutant.py

@ -2,7 +2,8 @@ import argparse
import inspect
import logging
import sys
from typing import Type, List, Any
from typing import Type, List, Any, Dict, Tuple
import os
from crytic_compile import cryticparser
@ -10,9 +11,10 @@ from slither import Slither
from slither.tools.mutator.mutators import all_mutators
from .mutators.abstract_mutator import AbstractMutator
from .utils.command_line import output_mutators
from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list
logging.basicConfig()
logger = logging.getLogger("Slither")
logger = logging.getLogger("Slither-Mutate")
logger.setLevel(logging.INFO)
@ -22,7 +24,6 @@ logger.setLevel(logging.INFO)
###################################################################################
###################################################################################
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597",
@ -39,6 +40,22 @@ def parse_args() -> argparse.Namespace:
default=False,
)
parser.add_argument(
"--test-cmd",
help="Command line needed to run the tests for your project"
)
parser.add_argument(
"--test-dir",
help="Directory of tests"
)
# parameter to ignore the interfaces, libraries
parser.add_argument(
"--ignore-dirs",
help="Directories to ignore"
)
# Initiate all the crytic config cli options
cryticparser.init(parser)
@ -73,16 +90,52 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods
def main() -> None:
args = parse_args()
print(args.codebase)
sl = Slither(args.codebase, **vars(args))
for compilation_unit in sl.compilation_units:
for M in _get_mutators():
m = M(compilation_unit)
m.mutate()
# print(os.path.isdir(args.codebase)) # provided file/folder
# arguments
test_command: str = args.test_cmd
test_directory: str = args.test_dir
paths_to_ignore: List[str] = args.ignore_dirs
# get all the contracts as a list from given codebase
sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore)
print("Starting Mutation Campaign in", args.codebase, "\n")
for filename in sol_file_list:
# slither object
sl = Slither(filename, **vars(args))
# folder where backup files and valid mutants created
output_folder = os.getcwd() + "/mutation_campaign"
# create a backup files
files_dict = backup_source_file(sl.source_code, output_folder)
# total count of valid mutants
total_count = 0
# mutation
try:
for compilation_unit_of_main_file in sl.compilation_units:
# compilation_unit_of_main_file = sl.compilation_units[-1]
# for i in compilation_unit_of_main_file.contracts:
# print(i.name)
for M in _get_mutators():
m = M(compilation_unit_of_main_file)
count = m.mutate(test_command, test_directory)
if count != None:
total_count = total_count + count
except Exception as e:
logger.error(e)
# transfer and delete the backup files
transfer_and_delete(files_dict)
# output
print(f"Done mutating, '{filename}'")
print(f"Valid mutant count: '{total_count}'\n")
print("Finished Mutation Campaign in", args.codebase, "\n")
# endregion

@ -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)

@ -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)

@ -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)

@ -4,9 +4,10 @@ from enum import Enum
from typing import Optional, Dict
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.tools.doctor.utils import snip_section
from slither.formatters.utils.patches import apply_patch, create_diff
logger = logging.getLogger("Slither")
logger = logging.getLogger("Slither-Mutate")
class IncorrectMutatorInitialization(Exception):
@ -68,13 +69,14 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
)
@abc.abstractmethod
def _mutate(self) -> Dict:
def _mutate(self, test_cmd: str, test_dir: str) -> Dict:
"""TODO Documentation"""
return {}
def mutate(self) -> None:
all_patches = self._mutate()
def mutate(self, testing_command: str, testing_directory: str) -> int:
# call _mutate function from different mutators
(all_patches, valid_mutant_count) = self._mutate(testing_command, testing_directory)
if "patches" not in all_patches:
logger.debug(f"No patches found by {self.NAME}")
return
@ -93,4 +95,11 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
diff = create_diff(self.compilation_unit, original_txt, patched_txt, file)
if not diff:
logger.info(f"Impossible to generate patch; empty {patches}")
# print the differences
print(diff)
return valid_mutant_count

@ -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:

@ -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:

@ -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

@ -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}')

@ -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
Loading…
Cancel
Save