Updated files

pull/2278/head
Vishnuram Rajkumar 9 months ago
parent 71f970f051
commit c8462b2433
  1. 8
      slither/tools/mutator/README.md
  2. 66
      slither/tools/mutator/__main__.py
  3. 6
      slither/tools/mutator/mutators/AOR.py
  4. 6
      slither/tools/mutator/mutators/ASOR.py
  5. 4
      slither/tools/mutator/mutators/BOR.py
  6. 13
      slither/tools/mutator/mutators/CR.py
  7. 13
      slither/tools/mutator/mutators/FHR.py
  8. 7
      slither/tools/mutator/mutators/LIR.py
  9. 4
      slither/tools/mutator/mutators/LOR.py
  10. 7
      slither/tools/mutator/mutators/MIA.py
  11. 3
      slither/tools/mutator/mutators/MVIE.py
  12. 3
      slither/tools/mutator/mutators/MVIV.py
  13. 7
      slither/tools/mutator/mutators/MWA.py
  14. 3
      slither/tools/mutator/mutators/ROR.py
  15. 10
      slither/tools/mutator/mutators/RR.py
  16. 20
      slither/tools/mutator/mutators/SBR.py
  17. 7
      slither/tools/mutator/mutators/UOR.py
  18. 30
      slither/tools/mutator/mutators/abstract_mutator.py
  19. 4
      slither/tools/mutator/mutators/all_mutators.py
  20. 60
      slither/tools/mutator/utils/file_handling.py
  21. 2
      slither/tools/mutator/utils/patch.py
  22. 54
      slither/tools/mutator/utils/testing_generated_mutant.py

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

@ -4,14 +4,14 @@ import logging
import sys
import os
import shutil
from typing import Type, List, Any
from typing import Type, List, Any, Optional
from crytic_compile import cryticparser
from slither import Slither
from slither.tools.mutator.mutators import all_mutators
from slither.utils.colors import yellow, magenta
from .mutators.abstract_mutator import AbstractMutator
from .utils.command_line import output_mutators
from .utils.file_handling import transfer_and_delete, backup_source_file, get_sol_file_list
from slither.utils.colors import yellow, magenta
logging.basicConfig()
logger = logging.getLogger("Slither-Mutate")
@ -24,12 +24,16 @@ logger.setLevel(logging.INFO)
###################################################################################
def parse_args() -> argparse.Namespace:
"""
Parse the underlying arguments for the program.
Returns: The arguments for the program.
"""
parser = argparse.ArgumentParser(
description="Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597",
usage="slither-mutate <codebase> --test-cmd <test command> <options>",
)
parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)")
parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)")
parser.add_argument(
"--list-mutators",
@ -108,7 +112,7 @@ def parse_args() -> argparse.Namespace:
def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]:
detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)]
if not mutators_list is None:
if mutators_list is not None:
detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) and str(c.NAME) in mutators_list ]
else:
detectors = [c for c in detectors_ if inspect.isclass(c) and issubclass(c, AbstractMutator) ]
@ -122,7 +126,6 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods
output_mutators(checks)
parser.exit()
# endregion
###################################################################################
###################################################################################
@ -130,46 +133,46 @@ class ListMutators(argparse.Action): # pylint: disable=too-few-public-methods
###################################################################################
###################################################################################
def main() -> None:
def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals
args = parse_args()
# arguments
test_command: str = args.test_cmd
test_directory: str = args.test_dir
paths_to_ignore: str | None = args.ignore_dirs
output_dir: str | None = args.output_dir
timeout: int | None = args.timeout
solc_remappings: str | None = args.solc_remaps
verbose: bool = args.verbose
mutators_to_run: List[str] | None = args.mutators_to_run
contract_names: List[str] | None = args.contract_names
quick_flag: bool = args.quick
print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n"))
test_directory: Optional[str] = args.test_dir
paths_to_ignore: Optional[str] = args.ignore_dirs
output_dir: Optional[str] = args.output_dir
timeout: Optional[int] = args.timeout
solc_remappings: Optional[str] = args.solc_remaps
verbose: Optional[bool] = args.verbose
mutators_to_run: Optional[List[str]] = args.mutators_to_run
contract_names: Optional[List[str]] = args.contract_names
quick_flag: Optional[bool] = args.quick
logger.info(magenta(f"Starting Mutation Campaign in '{args.codebase} \n"))
if paths_to_ignore:
paths_to_ignore_list = paths_to_ignore.strip('][').split(',')
print(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n"))
logger.info(magenta(f"Ignored paths - {', '.join(paths_to_ignore_list)} \n"))
else:
paths_to_ignore_list = []
# get all the contracts as a list from given codebase
# get all the contracts as a list from given codebase
sol_file_list: List[str] = get_sol_file_list(args.codebase, paths_to_ignore_list)
# folder where backup files and valid mutants created
if output_dir == None:
if output_dir is None:
output_dir = "/mutation_campaign"
output_folder = os.getcwd() + output_dir
if os.path.exists(output_folder):
shutil.rmtree(output_folder)
# set default timeout
if timeout == None:
if timeout is None:
timeout = 30
# setting RR mutator as first mutator
mutators_list = _get_mutators(mutators_to_run)
# insert RR and CR in front of the list
CR_RR_list = []
duplicate_list = mutators_list.copy()
@ -178,15 +181,15 @@ def main() -> None:
mutators_list.remove(M)
CR_RR_list.insert(0,M)
elif M.NAME == "CR":
mutators_list.remove(M)
mutators_list.remove(M)
CR_RR_list.insert(1,M)
mutators_list = CR_RR_list + mutators_list
for filename in sol_file_list:
for filename in sol_file_list: # pylint: disable=too-many-nested-blocks
contract_name = os.path.split(filename)[1].split('.sol')[0]
# slither object
sl = Slither(filename, **vars(args))
# create a backup files
# create a backup files
files_dict = backup_source_file(sl.source_code, output_folder)
# total count of mutants
total_count = 0
@ -200,7 +203,7 @@ def main() -> None:
for compilation_unit_of_main_file in sl.compilation_units:
contract_instance = ''
for contract in compilation_unit_of_main_file.contracts:
if contract_names != None and contract.name in contract_names:
if contract_names is not None and contract.name in contract_names:
contract_instance = contract
elif str(contract.name).lower() == contract_name.lower():
contract_instance = contract
@ -215,20 +218,19 @@ def main() -> None:
dont_mutate_lines = lines_list
if not quick_flag:
dont_mutate_lines = []
except Exception as e:
except Exception as e: # pylint: disable=broad-except
logger.error(e)
except KeyboardInterrupt:
# transfer and delete the backup files if interrupted
logger.error("\nExecution interrupted by user (Ctrl + C). Cleaning up...")
transfer_and_delete(files_dict)
# transfer and delete the backup files
transfer_and_delete(files_dict)
# output
print(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n"))
logger.info(yellow(f"Done mutating, '{filename}'. Valid mutant count: '{v_count}' and Total mutant count '{total_count}'.\n"))
print(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n"))
logger.info(magenta(f"Finished Mutation Campaign in '{args.codebase}' \n"))
# endregion

@ -18,11 +18,11 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
try:
ir_expression = node.expression
except:
except: # pylint: disable=bare-except
continue
for ir in node.irs:
if isinstance(ir, Binary) and ir.type in arithmetic_operators:
@ -40,4 +40,4 @@ class AOR(AbstractMutator): # pylint: disable=too-few-public-methods
# Replace the expression with true
new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}"
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -24,7 +24,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
for ir in node.irs:
if isinstance(ir.expression, AssignmentOperation) and ir.expression.type in assignment_operators:
@ -33,7 +33,7 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods
alternative_ops = assignment_operators[:]
try:
alternative_ops.remove(ir.expression.type)
except:
except: # pylint: disable=bare-except
continue
for op in alternative_ops:
if op != ir.expression:
@ -45,4 +45,4 @@ class ASOR(AbstractMutator): # pylint: disable=too-few-public-methods
# Replace the expression with true
new_str = f"{old_str.split(str(ir.expression.type))[0]}{op}{old_str.split(str(ir.expression.type))[1]}"
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -18,7 +18,7 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, Binary) and ir.type in bitwise_operators:
@ -34,4 +34,4 @@ class BOR(AbstractMutator): # pylint: disable=too-few-public-methods
# Replace the expression with true
new_str = f"{old_str.split(ir.type.value)[0]}{op.value}{old_str.split(ir.type.value)[1]}"
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

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

@ -1,7 +1,8 @@
from typing import Dict
import re
from slither.tools.mutator.utils.patch import create_patch_with_line
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator
import re
function_header_replacements = [
"pure ==> view",
@ -16,9 +17,8 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
# function_header = function.source_mapping.content.split('{')[0]
for function in self.contract.functions_and_modifiers_declared:
start = function.source_mapping.start
stop = start + function.source_mapping.content.find('{')
old_str = self.in_file_str[start:stop]
@ -27,8 +27,7 @@ class FHR(AbstractMutator): # pylint: disable=too-few-public-methods
for value in function_header_replacements:
left_value = value.split(" ==> ")[0]
right_value = value.split(" ==> ")[1]
if re.search(re.compile(left_value), old_str) != None:
if re.search(re.compile(left_value), old_str) is not None:
new_str = re.sub(re.compile(left_value), right_value, old_str)
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -11,12 +11,12 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods
NAME = "LIR"
HELP = "Literal Interger Replacement"
def _mutate(self) -> Dict:
def _mutate(self) -> Dict: # pylint: disable=too-many-branches
result: Dict = {}
variable: Variable
# Create fault for state variables declaration
for variable in self.contract.state_variables_declared:
for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks
if variable.initialized:
# Cannot remove the initialization of constant variables
if variable.is_constant:
@ -50,7 +50,7 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods
line_no[0]
)
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for variable in function.local_variables:
if variable.initialized and isinstance(variable.expression, Literal):
if isinstance(variable.type, ElementaryType):
@ -78,5 +78,4 @@ class LIR(AbstractMutator): # pylint: disable=too-few-public-methods
new_str,
line_no[0]
)
return result

@ -15,7 +15,7 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, Binary) and ir.type in logical_operators:
@ -32,4 +32,4 @@ class LOR(AbstractMutator): # pylint: disable=too-few-public-methods
# Replace the expression with true
new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}"
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -10,7 +10,6 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for node in function.nodes:
if node.type == NodeType.IF:
@ -24,9 +23,9 @@ class MIA(AbstractMutator): # pylint: disable=too-few-public-methods
for value in ["true", "false"]:
new_str = value
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
if not isinstance(node.expression, UnaryOperation):
new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')'
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -56,5 +56,4 @@ class MVIE(AbstractMutator): # pylint: disable=too-few-public-methods
new_str,
line_no[0]
)
return result
return result

@ -55,5 +55,4 @@ class MVIV(AbstractMutator): # pylint: disable=too-few-public-methods
new_str,
line_no[0]
)
return result
return result

@ -10,7 +10,7 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for node in function.nodes:
if node.type == NodeType.IFLOOP:
@ -22,5 +22,6 @@ class MWA(AbstractMutator): # pylint: disable=too-few-public-methods
if not line_no[0] in self.dont_mutate_line:
if not isinstance(node.expression, UnaryOperation):
new_str = str(UnaryOperationType.BANG) + '(' + old_str + ')'
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result

@ -19,7 +19,7 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, Binary) and ir.type in relational_operators:
@ -37,4 +37,3 @@ class ROR(AbstractMutator): # pylint: disable=too-few-public-methods
new_str = f"{old_str.split(ir.type.value)[0]} {op.value} {old_str.split(ir.type.value)[1]}"
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result

@ -12,20 +12,14 @@ class RR(AbstractMutator): # pylint: disable=too-few-public-methods
for function in self.contract.functions_and_modifiers_declared:
for node in function.nodes:
if node.type != NodeType.ENTRYPOINT and NodeType.ENDIF != node.type and NodeType.ENDLOOP != node.type:
if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP):
# Get the string
start = node.source_mapping.start
stop = start + node.source_mapping.length
old_str = self.in_file_str[start:stop]
old_str = self.in_file_str[start:stop]
line_no = node.source_mapping.lines
if not line_no[0] in self.dont_mutate_line:
if old_str != 'revert()':
new_str = 'revert()'
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result

@ -1,8 +1,8 @@
from typing import Dict
import re
from slither.core.cfg.node import NodeType
from slither.tools.mutator.utils.patch import create_patch_with_line
from slither.tools.mutator.mutators.abstract_mutator import AbstractMutator
import re
from slither.core.variables.variable import Variable
solidity_rules = [
@ -43,19 +43,17 @@ solidity_rules = [
"while ==> if",
]
class SBR(AbstractMutator): # pylint: disable=too-few-public-methods
NAME = "SBR"
HELP = 'Solidity Based Replacement'
def _mutate(self) -> Dict:
result: Dict = {}
variable: Variable
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
if node.type != NodeType.ENTRYPOINT and node.type != NodeType.ENDIF and node.type != NodeType.ENDLOOP:
if node.type not in (NodeType.ENTRYPOINT, NodeType.ENDIF, NodeType.ENDLOOP):
# Get the string
start = node.source_mapping.start
stop = start + node.source_mapping.length
@ -65,11 +63,11 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods
for value in solidity_rules:
left_value = value.split(" ==> ")[0]
right_value = value.split(" ==> ")[1]
if re.search(re.compile(left_value), old_str) != None:
if re.search(re.compile(left_value), old_str) is not None:
new_str = re.sub(re.compile(left_value), right_value, old_str)
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
for variable in self.contract.state_variables_declared:
for variable in self.contract.state_variables_declared: # pylint: disable=too-many-nested-blocks
node = variable.node_initialization
if node:
start = node.source_mapping.start
@ -80,13 +78,7 @@ class SBR(AbstractMutator): # pylint: disable=too-few-public-methods
for value in solidity_rules:
left_value = value.split(" ==> ")[0]
right_value = value.split(" ==> ")[1]
if re.search(re.compile(left_value), old_str) != None:
if re.search(re.compile(left_value), old_str) is not None:
new_str = re.sub(re.compile(left_value), right_value, old_str)
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result

@ -18,11 +18,11 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods
def _mutate(self) -> Dict:
result: Dict = {}
for function in self.contract.functions_and_modifiers_declared:
for function in self.contract.functions_and_modifiers_declared: # pylint: disable=too-many-nested-blocks
for node in function.nodes:
try:
ir_expression = node.expression
except:
except: # pylint: disable=bare-except
continue
start = node.source_mapping.start
stop = start + node.source_mapping.length
@ -47,5 +47,4 @@ class UOR(AbstractMutator): # pylint: disable=too-few-public-methods
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
new_str = str(variable_read) + str(op)
create_patch_with_line(result, self.in_file, start, stop, old_str, new_str, line_no[0])
return result
return result

@ -11,24 +11,24 @@ logger = logging.getLogger("Slither-Mutate")
class IncorrectMutatorInitialization(Exception):
pass
class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods
NAME = ""
HELP = ""
VALID_MUTANTS_COUNT = 0
INVALID_MUTANTS_COUNT = 0
def __init__(
self, compilation_unit: SlitherCompilationUnit,
timeout: int,
testing_command: str,
testing_directory: str,
contract_instance: Contract,
solc_remappings: str | None,
def __init__( # pylint: disable=too-many-arguments
self, compilation_unit: SlitherCompilationUnit,
timeout: int,
testing_command: str,
testing_directory: str,
contract_instance: Contract,
solc_remappings: str | None,
verbose: bool,
output_folder: str,
dont_mutate_line: List[int],
rate: int = 10,
rate: int = 10,
seed: Optional[int] = None
) -> None:
self.compilation_unit = compilation_unit
@ -60,7 +60,7 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
raise IncorrectMutatorInitialization(
f"rate must be between 0 and 100 {self.__class__.__name__}"
)
@abc.abstractmethod
def _mutate(self) -> Dict:
"""TODO Documentation"""
@ -70,19 +70,19 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
# call _mutate function from different mutators
(all_patches) = self._mutate()
if "patches" not in all_patches:
logger.debug(f"No patches found by {self.NAME}")
logger.debug("No patches found by %s", self.NAME)
return (0,0,self.dont_mutate_line)
for file in all_patches["patches"]:
original_txt = self.slither.source_code[file].encode("utf8")
patches = all_patches["patches"][file]
patches.sort(key=lambda x: x["start"])
print(yellow(f"Mutating {file} with {self.NAME} \n"))
logger.info(yellow(f"Mutating {file} with {self.NAME} \n"))
for patch in patches:
# test the patch
flag = test_patch(file, patch, self.test_command, self.VALID_MUTANTS_COUNT, self.NAME, self.timeout, self.solc_remappings, self.verbose)
# if RR or CR and valid mutant, add line no.
if (self.NAME == 'RR' or self.NAME == 'CR') and flag:
if self.NAME in ('RR', 'CR') and flag:
self.dont_mutate_line.append(patch['line_number'])
# count the valid and invalid mutants
if not flag:
@ -97,4 +97,4 @@ class AbstractMutator(metaclass=abc.ABCMeta): # pylint: disable=too-few-public-
# add valid mutant patches to a output file
with open(self.output_folder + "/patches_file.txt", 'a') as patches_file:
patches_file.write(diff + '\n')
return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line)
return (self.VALID_MUTANTS_COUNT, self.INVALID_MUTANTS_COUNT, self.dont_mutate_line)

@ -3,7 +3,7 @@ from slither.tools.mutator.mutators.MVIV import MVIV # severity low
from slither.tools.mutator.mutators.MVIE import MVIE # severity low
from slither.tools.mutator.mutators.LOR import LOR # severity medium
from slither.tools.mutator.mutators.UOR import UOR # severity medium
from slither.tools.mutator.mutators.SBR import SBR # severity medium
from slither.tools.mutator.mutators.SBR import SBR # severity medium
from slither.tools.mutator.mutators.AOR import AOR # severity medium
from slither.tools.mutator.mutators.BOR import BOR # severity medium
from slither.tools.mutator.mutators.ASOR import ASOR # severity medium
@ -13,4 +13,4 @@ from slither.tools.mutator.mutators.FHR import FHR # severity medium
from slither.tools.mutator.mutators.MIA import MIA # severity medium
from slither.tools.mutator.mutators.ROR import ROR # severity medium
from slither.tools.mutator.mutators.RR import RR # severity high
from slither.tools.mutator.mutators.CR import CR # severity high
from slither.tools.mutator.mutators.CR import CR # severity high

@ -6,47 +6,49 @@ logger = logging.getLogger("Slither-Mutate")
duplicated_files = {}
# function to backup the source file
def backup_source_file(source_code: Dict, output_folder: str) -> Dict:
"""
function to backup the source file
returns: dictionary of duplicated files
"""
os.makedirs(output_folder, exist_ok=True)
for file_path, content in source_code.items():
directory, filename = os.path.split(file_path)
new_filename = f"{output_folder}/backup_{filename}"
new_file_path = os.path.join(directory, new_filename)
with open(new_file_path, 'w') as new_file:
with open(new_file_path, 'w', encoding="utf8") as new_file:
new_file.write(content)
duplicated_files[file_path] = new_file_path
return duplicated_files
# function to transfer the original content to the sol file after campaign
def transfer_and_delete(files_dict: Dict) -> None:
"""function to transfer the original content to the sol file after campaign"""
try:
files_dict_copy = files_dict.copy()
for item, value in files_dict_copy.items():
with open(value, 'r') as duplicated_file:
for item, value in files_dict_copy.items():
with open(value, 'r', encoding="utf8") as duplicated_file:
content = duplicated_file.read()
with open(item, 'w') as original_file:
with open(item, 'w', encoding="utf8") as original_file:
original_file.write(content)
os.remove(value)
# delete elements from the global dict
del duplicated_files[item]
except Exception as e:
except Exception as e: # pylint: disable=broad-except
logger.error(f"Error transferring content: {e}")
#function to create new mutant file
def create_mutant_file(file: str, count: int, rule: str) -> None:
"""function to create new mutant file"""
try:
directory, filename = os.path.split(file)
_, filename = os.path.split(file)
# Read content from the duplicated file
with open(file, 'r') as source_file:
with open(file, 'r', encoding="utf8") as source_file:
content = source_file.read()
# Write content to the original file
@ -54,42 +56,46 @@ def create_mutant_file(file: str, count: int, rule: str) -> None:
# create folder for each contract
os.makedirs("mutation_campaign/" + mutant_name, exist_ok=True)
with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w') as mutant_file:
with open("mutation_campaign/" + mutant_name + '/' + mutant_name + '_' + rule + '_' + str(count) + '.sol', 'w', encoding="utf8") as mutant_file:
mutant_file.write(content)
# reset the file
with open(duplicated_files[file], 'r') as duplicated_file:
with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file:
duplicate_content = duplicated_file.read()
with open(file, 'w') as source_file:
with open(file, 'w', encoding="utf8") as source_file:
source_file.write(duplicate_content)
except Exception as e:
except Exception as e: # pylint: disable=broad-except
logger.error(f"Error creating mutant: {e}")
# function to reset the file
def reset_file(file: str) -> None:
"""function to reset the file"""
try:
# directory, filename = os.path.split(file)
# reset the file
with open(duplicated_files[file], 'r') as duplicated_file:
with open(duplicated_files[file], 'r', encoding="utf8") as duplicated_file:
duplicate_content = duplicated_file.read()
with open(file, 'w') as source_file:
with open(file, 'w', encoding="utf8") as source_file:
source_file.write(duplicate_content)
except Exception as e:
except Exception as e: # pylint: disable=broad-except
logger.error(f"Error resetting file: {e}")
# function to get the contracts list
def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str]:
"""
function to get the contracts list
returns: list of .sol files
"""
sol_file_list = []
if ignore_paths == None:
if ignore_paths is None:
ignore_paths = []
# if input is contract file
if os.path.isfile(codebase):
return [codebase]
# if input is folder
elif os.path.isdir(codebase):
directory = os.path.abspath(codebase)
@ -98,10 +104,10 @@ def get_sol_file_list(codebase: str, ignore_paths: List[str] | None) -> List[str
if os.path.isfile(filename):
sol_file_list.append(filename)
elif os.path.isdir(filename):
directory_name, dirname = os.path.split(filename)
_, dirname = os.path.split(filename)
if dirname in ignore_paths:
continue
continue
for i in get_sol_file_list(filename, ignore_paths):
sol_file_list.append(i)
return sol_file_list
return sol_file_list

@ -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)
result["patches"][file].append(p)

@ -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
print(red(f"String '{patch['old_string']}' replaced with '{patch['new_string']}' at line no. '{patch['line_number']}' ---> INVALID\n"))
return False

Loading…
Cancel
Save