Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgraded Slither-mutate #2278

Merged
merged 16 commits into from
Jan 29, 2024
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

```
positional arguments:
codebase Codebase to analyze (.sol file, truffle directory, ...)
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved

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
```
190 changes: 168 additions & 22 deletions slither/tools/mutator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
import inspect
import logging
import sys
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
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")
logger = logging.getLogger("Slither-Mutate")
logger.setLevel(logging.INFO)


###################################################################################
###################################################################################
# region Cli Arguments
###################################################################################
###################################################################################


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 <codebase> --test-cmd <test command> <options>",
)

parser.add_argument("codebase", help="Codebase to analyze (.sol file, truffle directory, ...)")
Expand All @@ -39,6 +39,64 @@
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 @@ -48,18 +106,19 @@

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
) -> None: # pylint: disable=signature-differs
checks = _get_mutators()
checks = _get_mutators(None)
output_mutators(checks)
parser.exit()

Expand All @@ -71,18 +130,105 @@
###################################################################################
###################################################################################


def main() -> None:

Check warning on line 133 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

R0914: Too many local variables (33/15) (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: 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

Check warning on line 144 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
contract_names: List[str] | None = args.contract_names
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved
quick_flag: bool = args.quick

Check warning on line 147 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
print(magenta(f"Starting Mutation Campaign in '{args.codebase} \n"))
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved

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

Check warning on line 156 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
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:

Check warning on line 160 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0121: Comparison 'output_dir == None' should be 'output_dir is None' (singleton-comparison)
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved
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:
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved
timeout = 30

# setting RR mutator as first mutator
mutators_list = _get_mutators(mutators_to_run)

Check warning on line 172 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
# 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)

Check warning on line 181 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
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]
# slither object
sl = Slither(filename, **vars(args))
# create a backup files

Check warning on line 189 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
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 != None and contract.name in contract_names:
0xalpharush marked this conversation as resolved.
Show resolved Hide resolved
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:
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)

Check warning on line 225 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
# transfer and delete the backup files
transfer_and_delete(files_dict)

Check warning on line 228 in slither/tools/mutator/__main__.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

C0303: Trailing whitespace (trailing-whitespace)
# output
print(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"))
# endregion

43 changes: 43 additions & 0 deletions slither/tools/mutator/mutators/AOR.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 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:
# 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
48 changes: 48 additions & 0 deletions slither/tools/mutator/mutators/ASOR.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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

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"

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 alternative_ops:
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
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
Loading