Skip to content

Commit

Permalink
Merge pull request #2278 from vishnuram1999/slither-mutate-upgrade
Browse files Browse the repository at this point in the history
Upgraded Slither-mutate
  • Loading branch information
0xalpharush authored Jan 29, 2024
2 parents 40536d8 + 360509f commit afa604d
Show file tree
Hide file tree
Showing 24 changed files with 1,403 additions and 191 deletions.
33 changes: 33 additions & 0 deletions slither/tools/mutator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Slither-mutate

`slither-mutate` is a mutation testing tool for solidity based smart contracts.

## Usage

`slither-mutate <codebase> --test-cmd <test-command> <options>`

To view the list of mutators available `slither-mutate --list-mutators`

### CLI Interface

```shell
positional arguments:
codebase Codebase to analyze (.sol file, project directory, ...)

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')
--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
```
204 changes: 185 additions & 19 deletions slither/tools/mutator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
import inspect
import logging
import sys
from typing import Type, List, Any

import os
import shutil
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,
)

logging.basicConfig()
logger = logging.getLogger("Slither")
logger = logging.getLogger("Slither-Mutate")
logger.setLevel(logging.INFO)


###################################################################################
###################################################################################
# region Cli Arguments
Expand All @@ -24,12 +29,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 target",
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",
Expand All @@ -39,6 +48,51 @@ def parse_args() -> argparse.Namespace:
default=False,
)

# argument to add the test command
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")

# argument to ignore the interfaces, libraries
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)")

# output directory argument
parser.add_argument(
"--output-dir", help="Name of 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",
)

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

Expand All @@ -49,17 +103,26 @@ 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 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)]
return detectors


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

Expand All @@ -72,17 +135,120 @@ def __call__(
###################################################################################


def main() -> None:

def main() -> (None): # pylint: disable=too-many-statements,too-many-branches,too-many-locals
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()
# arguments
test_command: str = args.test_cmd
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(",")
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
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 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 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()
for M in duplicate_list:
if M.NAME == "RR":
mutators_list.remove(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: # 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
files_dict = backup_source_file(sl.source_code, output_folder)
# total count of mutants
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:
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 == "":
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,
)
(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
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
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
54 changes: 54 additions & 0 deletions slither/tools/mutator/mutators/AOR.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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
from slither.core.expressions.unary_operation import UnaryOperation

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"

def _mutate(self) -> Dict:
result: Dict = {}
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
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:
# 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:
# 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
Loading

0 comments on commit afa604d

Please sign in to comment.