From 46e43fc8c88a3c1853a50c3b67c0bb84c22d8e25 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 16:16:17 +0100 Subject: [PATCH 01/14] feat: function-filter tool scaffold --- setup.py | 1 + slither/tools/function_filter/__init__.py | 0 slither/tools/function_filter/__main__.py | 109 ++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 slither/tools/function_filter/__init__.py create mode 100644 slither/tools/function_filter/__main__.py diff --git a/setup.py b/setup.py index 332f8fc183..644688b8eb 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ "slither-doctor = slither.tools.doctor.__main__:main", "slither-documentation = slither.tools.documentation.__main__:main", "slither-interface = slither.tools.interface.__main__:main", + "slither-function-filter = slither.tools.function_filter.__main__:main", ] }, ) diff --git a/slither/tools/function_filter/__init__.py b/slither/tools/function_filter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py new file mode 100644 index 0000000000..59c79e1037 --- /dev/null +++ b/slither/tools/function_filter/__main__.py @@ -0,0 +1,109 @@ +import sys +import logging +from argparse import ArgumentParser, Namespace + +from crytic_compile import cryticparser +from slither import Slither +from slither.core.declarations import Function +from slither.utils.colors import green + +logging.basicConfig() +logging.getLogger("Slither").setLevel(logging.INFO) + + +def parse_args() -> Namespace: + """ + Parse the underlying arguments for the program. + :return: Returns the arguments for the program. + """ + parser: ArgumentParser = ArgumentParser( + description="FunctionSummarySelection", + usage="function_summary_selection.py filename [options]", + ) + + parser.add_argument( + "filename", help="The filename of the contract or truffle directory to analyze." + ) + + parser.add_argument( + "--contract-name", type=str, help="If set, filter functions declared only in that contract." + ) + + parser.add_argument("--visibility", type=str, help="Visibility of the functions.") + + parser.add_argument( + "--modifiers", action="store_true", help="If set, filter functions that have modifiers." + ) + + parser.add_argument( + "--ext-calls", + action="store_true", + help="If set, filter functions that make external calls.", + ) + + parser.add_argument( + "--int-calls", + action="store_true", + help="If set, filter functions that make internal calls.", + ) + + parser.add_argument( + "--state-change", action="store_true", help="If set, filter functions that change state." + ) + + cryticparser.init(parser) + + return parser.parse_args() + + +def main() -> None: + # ------------------------------ + # FunctionSummarySelection.py + # Usage: python3 function_summary_selection.py filename [options] + # Example: python3 function_summary_selection.py contract.sol ---- + # ------------------------------ + args = parse_args() + + # Perform slither analysis on the given filename + # args's empty + slither = Slither(args.filename, **vars(args)) + + # Access the arguments + contract_name = args.contract_name + visibility = args.visibility + modifiers = args.modifiers + ext_calls = args.ext_calls + int_calls = args.int_calls + state_change = args.state_change + + for contract in slither.contracts: + if contract.name == contract_name: + # get all contracts for target contract, drop interfaces + contracts_inherited = [ + parent for parent in contract.immediate_inheritance if not parent.is_interface + ] + + for function in contract.functions: + print("contract.name == contract_name, contract.functions", function.name) + + for function in contracts_inherited: + print( + "contract.name == contract_name, function in contracts_inherited", function.name + ) + + else: + for function in contract.functions: + print("contract.name != contract_name", function.name) + + # # Extract function summaries based on selected options + # function_summaries = summarize_functions(slither, args.options if args.options else range(1, 8)) + + # # Output the function summaries + # for function_summary in function_summaries: + # print(green(function_summary)) + + print("\n") + + +if __name__ == "__main__": + main() From e388cb43f07aa536adeb3bf5905f00dc43916283 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 17:14:44 +0100 Subject: [PATCH 02/14] feat: working selection of contracts to parse --- slither/tools/function_filter/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 59c79e1037..d264ef2d3a 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -77,6 +77,7 @@ def main() -> None: state_change = args.state_change for contract in slither.contracts: + if contract.name == contract_name: # get all contracts for target contract, drop interfaces contracts_inherited = [ @@ -90,8 +91,10 @@ def main() -> None: print( "contract.name == contract_name, function in contracts_inherited", function.name ) + + break - else: + if not contract_name: for function in contract.functions: print("contract.name != contract_name", function.name) From 56c72cbe3037da51155330c2b97654488fc22ff7 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 22:51:48 +0100 Subject: [PATCH 03/14] testing: freeze --- slither/tools/function_filter/__main__.py | 102 +++++++++++++++------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index d264ef2d3a..d07855f996 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -47,66 +47,102 @@ def parse_args() -> Namespace: help="If set, filter functions that make internal calls.", ) - parser.add_argument( - "--state-change", action="store_true", help="If set, filter functions that change state." - ) + # parser.add_argument( + # "--state-change", action="store_true", help="If set, filter functions that change state." + # ) cryticparser.init(parser) return parser.parse_args() +def filter_function(function: Function, args) -> dict[str, str]: + summary = function.get_summary() + + data = { + "contract_name": summary[0], + "function_sig": summary[1], + "visibility": summary[2], + "modifiers": summary[3], + "vars_read": summary[4], + "vars_written": summary[5], + "internal_calls": summary[6], + "external_calls": summary[7], + "cyclomatic_complexity": summary[8], + "flags" : [] + # "flags": { + # "visibility": False, + # "modifiers": False, + # "ext-calls": False, + # "int-calls": False, + # }, + } + + # Check visibility + if args.visibility and function.visibility != args.visibility: + data["flags"].append(True) + # data["flags"]["visibility"] = True + + # Check for modifiers + if args.modifiers: + if data["modifiers"] is not None: + data["flags"]["modifiers"] = True + + # Check for external calls + if args.ext_calls: + if data["external_calls"] is not None: + data["flags"]["ext-calls"] = True + + # Check for internal calls + if args.int_calls: + if data["internal_calls"] is not None: + data["flags"]["int-calls"] = True + + def main() -> None: - # ------------------------------ - # FunctionSummarySelection.py - # Usage: python3 function_summary_selection.py filename [options] - # Example: python3 function_summary_selection.py contract.sol ---- - # ------------------------------ args = parse_args() # Perform slither analysis on the given filename - # args's empty slither = Slither(args.filename, **vars(args)) # Access the arguments contract_name = args.contract_name - visibility = args.visibility - modifiers = args.modifiers - ext_calls = args.ext_calls - int_calls = args.int_calls - state_change = args.state_change + # Store list + args_list = [args.visibility, args.modifiers, args.ext_calls, args.int_calls] + filter_results = [] for contract in slither.contracts: - + # Scan only target contract's functions (declared and inherited) if contract.name == contract_name: - # get all contracts for target contract, drop interfaces + # Find directly inherited contracts contracts_inherited = [ parent for parent in contract.immediate_inheritance if not parent.is_interface ] + # Iterate declared functions for function in contract.functions: - print("contract.name == contract_name, contract.functions", function.name) + # data = function_matches_criteria(function, args) + filter_results.append(filter_function(function, args)) + # Iterate inherited functions for function in contracts_inherited: - print( - "contract.name == contract_name, function in contracts_inherited", function.name - ) - - break + filter_results.append(filter_function(function, args)) + # Scan everything if no target contract is specified if not contract_name: for function in contract.functions: - print("contract.name != contract_name", function.name) - - # # Extract function summaries based on selected options - # function_summaries = summarize_functions(slither, args.options if args.options else range(1, 8)) - - # # Output the function summaries - # for function_summary in function_summaries: - # print(green(function_summary)) - - print("\n") - + filter_results.append(filter_function(function, args)) + + for target in filter_results[:]: + # Check if any of the flags is False and its corresponding arg is set + if (args.visibility and not target["flags"]["visibility"]) or \ + (args.modifiers and not target["flags"]["modifiers"]) or \ + (args.ext_calls and not target["flags"]["ext-calls"]) or \ + (args.int_calls and not target["flags"]["int-calls"]): + filter_results.remove(target) + + print(filter_results) + if __name__ == "__main__": main() From 7a7fe5e0573e95fed4ac831c09edd2da350f99f1 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 23:00:38 +0100 Subject: [PATCH 04/14] feat: basic filtering works --- slither/tools/function_filter/__main__.py | 69 ++++++++--------------- 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index d07855f996..494399d657 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -55,49 +55,28 @@ def parse_args() -> Namespace: return parser.parse_args() - -def filter_function(function: Function, args) -> dict[str, str]: - summary = function.get_summary() - - data = { - "contract_name": summary[0], - "function_sig": summary[1], - "visibility": summary[2], - "modifiers": summary[3], - "vars_read": summary[4], - "vars_written": summary[5], - "internal_calls": summary[6], - "external_calls": summary[7], - "cyclomatic_complexity": summary[8], - "flags" : [] - # "flags": { - # "visibility": False, - # "modifiers": False, - # "ext-calls": False, - # "int-calls": False, - # }, - } - +def filter_function(function: Function, args) -> bool: # Check visibility if args.visibility and function.visibility != args.visibility: - data["flags"].append(True) - # data["flags"]["visibility"] = True + return False # Check for modifiers if args.modifiers: - if data["modifiers"] is not None: - data["flags"]["modifiers"] = True + if not function.modifiers(): + return False # Check for external calls if args.ext_calls: - if data["external_calls"] is not None: - data["flags"]["ext-calls"] = True + if not function.high_level_calls(): + return False # Check for internal calls if args.int_calls: - if data["internal_calls"] is not None: - data["flags"]["int-calls"] = True + if not function.internal_calls(): + return False + # If none of the conditions have returned False, the function matches all provided criteria + return True def main() -> None: args = parse_args() @@ -108,7 +87,6 @@ def main() -> None: # Access the arguments contract_name = args.contract_name # Store list - args_list = [args.visibility, args.modifiers, args.ext_calls, args.int_calls] filter_results = [] for contract in slither.contracts: @@ -121,27 +99,26 @@ def main() -> None: # Iterate declared functions for function in contract.functions: - # data = function_matches_criteria(function, args) - filter_results.append(filter_function(function, args)) + if filter_function(function, args): + filter_results.append(function.get_summary()) # Iterate inherited functions - for function in contracts_inherited: - filter_results.append(filter_function(function, args)) + for contracts in contracts_inherited: + for function in contracts.functions: + if filter_function(function, args): + filter_results.append(function.get_summary()) # Scan everything if no target contract is specified if not contract_name: for function in contract.functions: - filter_results.append(filter_function(function, args)) - - for target in filter_results[:]: - # Check if any of the flags is False and its corresponding arg is set - if (args.visibility and not target["flags"]["visibility"]) or \ - (args.modifiers and not target["flags"]["modifiers"]) or \ - (args.ext_calls and not target["flags"]["ext-calls"]) or \ - (args.int_calls and not target["flags"]["int-calls"]): - filter_results.remove(target) + if filter_function(function, args): + filter_results.append(function.get_summary()) - print(filter_results) + if filter_results: + for result in filter_results: + print(result) + else: + print("No results found.") if __name__ == "__main__": From 465d5826e80e1dea153091b7622abc83f43a5b34 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 23:11:40 +0100 Subject: [PATCH 05/14] chore: minor changes to basic filtering, add state-change flag --- slither/tools/function_filter/__main__.py | 60 +++++++++++++---------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 494399d657..1ce1a23039 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -47,37 +47,44 @@ def parse_args() -> Namespace: help="If set, filter functions that make internal calls.", ) - # parser.add_argument( - # "--state-change", action="store_true", help="If set, filter functions that change state." - # ) + parser.add_argument( + "--state-change", action="store_true", help="If set, filter functions that change state." + ) cryticparser.init(parser) return parser.parse_args() + def filter_function(function: Function, args) -> bool: # Check visibility if args.visibility and function.visibility != args.visibility: return False - # Check for modifiers + # Check for existence of modifiers if args.modifiers: - if not function.modifiers(): + if not function.modifiers: return False - # Check for external calls + # Check for existence of external calls if args.ext_calls: - if not function.high_level_calls(): + if not function.high_level_calls: return False - # Check for internal calls + # Check for existence of internal calls if args.int_calls: - if not function.internal_calls(): + if not function.internal_calls: + return False + + # Check if function potentially changes state + if args.state_change: + if not function._view or not function._pure: return False # If none of the conditions have returned False, the function matches all provided criteria return True + def main() -> None: args = parse_args() @@ -91,35 +98,34 @@ def main() -> None: for contract in slither.contracts: # Scan only target contract's functions (declared and inherited) - if contract.name == contract_name: - # Find directly inherited contracts - contracts_inherited = [ - parent for parent in contract.immediate_inheritance if not parent.is_interface - ] - - # Iterate declared functions - for function in contract.functions: - if filter_function(function, args): - filter_results.append(function.get_summary()) - - # Iterate inherited functions - for contracts in contracts_inherited: - for function in contracts.functions: + if contract_name: + if contract.name == contract_name: + # Find directly inherited contracts + contracts_inherited = [ + parent for parent in contract.immediate_inheritance if not parent.is_interface + ] + + # Iterate declared functions + for function in contract.functions: if filter_function(function, args): filter_results.append(function.get_summary()) - # Scan everything if no target contract is specified - if not contract_name: + # Iterate inherited functions + for contracts in contracts_inherited: + for function in contracts.functions: + if filter_function(function, args): + filter_results.append(function.get_summary()) + else: for function in contract.functions: if filter_function(function, args): filter_results.append(function.get_summary()) - + if filter_results: for result in filter_results: print(result) else: print("No results found.") - + if __name__ == "__main__": main() From a0df8382e79d9f250171515c7cc4b3b3e21bdf99 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 23:49:18 +0100 Subject: [PATCH 06/14] feat: better cli output --- slither/tools/function_filter/__main__.py | 31 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 1ce1a23039..3caa37f202 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -5,10 +5,11 @@ from crytic_compile import cryticparser from slither import Slither from slither.core.declarations import Function -from slither.utils.colors import green +from slither.utils.colors import green, bold, blue logging.basicConfig() -logging.getLogger("Slither").setLevel(logging.INFO) +logger = logging.getLogger("Slither-function-filter") +logger.setLevel(logging.INFO) def parse_args() -> Namespace: @@ -115,16 +116,38 @@ def main() -> None: for function in contracts.functions: if filter_function(function, args): filter_results.append(function.get_summary()) + + # Scan all contracts in the SourceMapping of filename provided else: for function in contract.functions: if filter_function(function, args): filter_results.append(function.get_summary()) if filter_results: + logger.info(green(f"Found {len(filter_results)} functions matching flags\n")) for result in filter_results: - print(result) + ( + contract_name, + function_sig, + visibility, + modifiers, + vars_read, + vars_written, + internal_calls, + external_calls, + cyclomatic_complexity, + ) = result + + logger.info(bold(f"Function: {contract_name}.{function_sig}")) + logger.info(blue(f"Visibility: {visibility}")) + logger.info(blue(f"Modifiers: {modifiers}")) + logger.info(blue(f"Variables Read: {vars_read}")) + logger.info(blue(f"Variables Written: {vars_written}")) + logger.info(blue(f"Internal Calls: {internal_calls}")) + logger.info(blue(f"External Calls: {external_calls}")) + logger.info(blue(f"Cyclomatic Complexity: {cyclomatic_complexity}\n")) else: - print("No results found.") + logger.info("No results found.") if __name__ == "__main__": From 82e91246ef9eeb84c27fea98b8c442c0afbe3bc0 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 18 Nov 2023 23:57:29 +0100 Subject: [PATCH 07/14] feat: finished basic function-filter --- slither/tools/function_filter/__main__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 3caa37f202..fc7b83f3df 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -5,7 +5,7 @@ from crytic_compile import cryticparser from slither import Slither from slither.core.declarations import Function -from slither.utils.colors import green, bold, blue +from slither.utils.colors import green, blue, red, bold logging.basicConfig() logger = logging.getLogger("Slither-function-filter") @@ -51,6 +51,10 @@ def parse_args() -> Namespace: parser.add_argument( "--state-change", action="store_true", help="If set, filter functions that change state." ) + + parser.add_argument( + "--read-only", action="store_true", help="If set, filter functions that do not change state." + ) cryticparser.init(parser) @@ -79,7 +83,12 @@ def filter_function(function: Function, args) -> bool: # Check if function potentially changes state if args.state_change: - if not function._view or not function._pure: + if function._view or function._pure: + return False + + # Check if function is read-only + if args.read_only: + if not (function._view or function._pure): return False # If none of the conditions have returned False, the function matches all provided criteria @@ -147,7 +156,7 @@ def main() -> None: logger.info(blue(f"External Calls: {external_calls}")) logger.info(blue(f"Cyclomatic Complexity: {cyclomatic_complexity}\n")) else: - logger.info("No results found.") + logger.info(red("No functions matching flags found.")) if __name__ == "__main__": From d1250d6f715aa499e586a151d79e12d6cd0b5453 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Wed, 22 Nov 2023 22:53:41 +0100 Subject: [PATCH 08/14] feat/fix: --declared-only flag for better filtering on search --- slither/tools/function_filter/__main__.py | 64 +++++++++++++++-------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index fc7b83f3df..55542fde78 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -1,4 +1,3 @@ -import sys import logging from argparse import ArgumentParser, Namespace @@ -18,8 +17,8 @@ def parse_args() -> Namespace: :return: Returns the arguments for the program. """ parser: ArgumentParser = ArgumentParser( - description="FunctionSummarySelection", - usage="function_summary_selection.py filename [options]", + description="Return contract functions based on the provided criteria.", + usage="slither-function-filter filename [options]", ) parser.add_argument( @@ -27,7 +26,15 @@ def parse_args() -> Namespace: ) parser.add_argument( - "--contract-name", type=str, help="If set, filter functions declared only in that contract." + "--contract-name", + type=str, + help="If set, filter functions declared and inherited in the specified contract.", + ) + + parser.add_argument( + "--declared-only", + action="store_true", + help="If set, filter functions only declared in the --contract-name.", ) parser.add_argument("--visibility", type=str, help="Visibility of the functions.") @@ -51,9 +58,11 @@ def parse_args() -> Namespace: parser.add_argument( "--state-change", action="store_true", help="If set, filter functions that change state." ) - + parser.add_argument( - "--read-only", action="store_true", help="If set, filter functions that do not change state." + "--read-only", + action="store_true", + help="If set, filter functions that do not change state.", ) cryticparser.init(parser) @@ -83,12 +92,12 @@ def filter_function(function: Function, args) -> bool: # Check if function potentially changes state if args.state_change: - if function._view or function._pure: + if function.view or function.pure: return False - + # Check if function is read-only if args.read_only: - if not (function._view or function._pure): + if not (function.view or function.pure): return False # If none of the conditions have returned False, the function matches all provided criteria @@ -109,23 +118,34 @@ def main() -> None: for contract in slither.contracts: # Scan only target contract's functions (declared and inherited) if contract_name: + # Match --contract-name with slither's contract object if contract.name == contract_name: - # Find directly inherited contracts - contracts_inherited = [ - parent for parent in contract.immediate_inheritance if not parent.is_interface - ] - - # Iterate declared functions - for function in contract.functions: - if filter_function(function, args): - filter_results.append(function.get_summary()) - - # Iterate inherited functions - for contracts in contracts_inherited: - for function in contracts.functions: + # Only target contract's declared functions are scanned + if args.declared_only: + # Iterate declared functions + for function in contract.functions: + if filter_function(function, args): + filter_results.append(function.get_summary()) + + # All functions (declared and inherited) are scanned + else: + contracts_inherited = [ + parent + for parent in contract.immediate_inheritance + if not parent.is_interface + ] + + # Iterate declared functions + for function in contract.functions: if filter_function(function, args): filter_results.append(function.get_summary()) + # Iterate inherited functions + for contracts in contracts_inherited: + for function in contracts.functions: + if filter_function(function, args): + filter_results.append(function.get_summary()) + # Scan all contracts in the SourceMapping of filename provided else: for function in contract.functions: From fdfffa52da14248442461c5962d3744a60f74254 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Thu, 23 Nov 2023 02:13:10 +0100 Subject: [PATCH 09/14] fix: simpler selection --- slither/tools/function_filter/__main__.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 55542fde78..22dc4a338e 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -116,36 +116,22 @@ def main() -> None: filter_results = [] for contract in slither.contracts: - # Scan only target contract's functions (declared and inherited) + # Scan only target contract's functions (declared only or immediate inheritance) if contract_name: # Match --contract-name with slither's contract object if contract.name == contract_name: # Only target contract's declared functions are scanned if args.declared_only: - # Iterate declared functions - for function in contract.functions: + for function in contract.functions_declared: if filter_function(function, args): filter_results.append(function.get_summary()) - # All functions (declared and inherited) are scanned + # All functions (declared and inherited) of target contract are scanned else: - contracts_inherited = [ - parent - for parent in contract.immediate_inheritance - if not parent.is_interface - ] - - # Iterate declared functions for function in contract.functions: if filter_function(function, args): filter_results.append(function.get_summary()) - # Iterate inherited functions - for contracts in contracts_inherited: - for function in contracts.functions: - if filter_function(function, args): - filter_results.append(function.get_summary()) - # Scan all contracts in the SourceMapping of filename provided else: for function in contract.functions: From 87545a584070200ff3c493dda570383326397729 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Thu, 11 Apr 2024 20:33:04 +0200 Subject: [PATCH 10/14] feat: line reference / more filters --- slither/tools/function_filter/__main__.py | 58 ++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 22dc4a338e..1494a9aa29 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -65,6 +65,30 @@ def parse_args() -> Namespace: help="If set, filter functions that do not change state.", ) + parser.add_argument( + "--contains-asm", + action="store_true", + help="If set, filter functions that contain inline assembly.", + ) + + parser.add_argument( + "--low-lvl-calls", + action="store_true", + help="If set, filter functions that make low level calls.", + ) + + parser.add_argument( + "--full-name", + type=str, + help="If set, filter functions by their full name.", + ) + + parser.add_argument( + "--in-source", + type=str, + help="If set, filter functions by the string in their source.", + ) + cryticparser.init(parser) return parser.parse_args() @@ -100,6 +124,26 @@ def filter_function(function: Function, args) -> bool: if not (function.view or function.pure): return False + # Check for existence of inline assembly + if args.contains_asm: + if not function.contains_asm: + return False + + # Check for existence of low level calls + if args.low_lvl_calls: + if not function.low_level_calls: + return False + + # Check for specific function name + if args.full_name: + if args.full_name not in function.full_name: + return False + + # Check for specific string in function source + if args.in_source: + if args.in_source not in function.source_mapping.content: + return False + # If none of the conditions have returned False, the function matches all provided criteria return True @@ -124,19 +168,29 @@ def main() -> None: if args.declared_only: for function in contract.functions_declared: if filter_function(function, args): + lines = ( + function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() + ) + summary = function.get_summary() + (lines,) filter_results.append(function.get_summary()) # All functions (declared and inherited) of target contract are scanned else: for function in contract.functions: if filter_function(function, args): + lines = ( + function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() + ) + summary = function.get_summary() + (lines,) filter_results.append(function.get_summary()) # Scan all contracts in the SourceMapping of filename provided else: for function in contract.functions: if filter_function(function, args): - filter_results.append(function.get_summary()) + lines = function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() + summary = function.get_summary() + (lines,) + filter_results.append(summary) if filter_results: logger.info(green(f"Found {len(filter_results)} functions matching flags\n")) @@ -151,9 +205,11 @@ def main() -> None: internal_calls, external_calls, cyclomatic_complexity, + lines, ) = result logger.info(bold(f"Function: {contract_name}.{function_sig}")) + logger.info(bold(f"Reference: {lines}")) logger.info(blue(f"Visibility: {visibility}")) logger.info(blue(f"Modifiers: {modifiers}")) logger.info(blue(f"Variables Read: {vars_read}")) From ba9cb0462edb2cffb11e7f7ca2f7fef3239dd4ab Mon Sep 17 00:00:00 2001 From: shortdoom Date: Thu, 11 Apr 2024 20:46:53 +0200 Subject: [PATCH 11/14] fix: typo in filter_results.append() --- slither/tools/function_filter/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 1494a9aa29..1e8c441797 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -171,8 +171,9 @@ def main() -> None: lines = ( function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() ) + print(lines) summary = function.get_summary() + (lines,) - filter_results.append(function.get_summary()) + filter_results.append(summary) # All functions (declared and inherited) of target contract are scanned else: @@ -182,7 +183,7 @@ def main() -> None: function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() ) summary = function.get_summary() + (lines,) - filter_results.append(function.get_summary()) + filter_results.append(summary) # Scan all contracts in the SourceMapping of filename provided else: From 06c8527771fe23784d0517fbc0a9c1a665b6e0b0 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Thu, 11 Apr 2024 22:24:15 +0200 Subject: [PATCH 12/14] refactor: make lint happy --- slither/tools/function_filter/__main__.py | 182 +++++----------------- 1 file changed, 43 insertions(+), 139 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 1e8c441797..d7bc1c9aac 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -12,11 +12,7 @@ def parse_args() -> Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser: ArgumentParser = ArgumentParser( + parser = ArgumentParser( description="Return contract functions based on the provided criteria.", usage="slither-function-filter filename [options]", ) @@ -24,69 +20,41 @@ def parse_args() -> Namespace: parser.add_argument( "filename", help="The filename of the contract or truffle directory to analyze." ) - parser.add_argument( "--contract-name", type=str, - help="If set, filter functions declared and inherited in the specified contract.", + help="Filter functions declared and inherited in the specified contract.", ) - parser.add_argument( "--declared-only", action="store_true", - help="If set, filter functions only declared in the --contract-name.", + help="Filter only functions declared in the --contract-name.", ) - parser.add_argument("--visibility", type=str, help="Visibility of the functions.") - - parser.add_argument( - "--modifiers", action="store_true", help="If set, filter functions that have modifiers." - ) - parser.add_argument( - "--ext-calls", - action="store_true", - help="If set, filter functions that make external calls.", + "--modifiers", action="store_true", help="Filter functions that have modifiers." ) - parser.add_argument( - "--int-calls", - action="store_true", - help="If set, filter functions that make internal calls.", + "--ext-calls", action="store_true", help="Filter functions that make external calls." ) - parser.add_argument( - "--state-change", action="store_true", help="If set, filter functions that change state." + "--int-calls", action="store_true", help="Filter functions that make internal calls." ) - parser.add_argument( - "--read-only", - action="store_true", - help="If set, filter functions that do not change state.", + "--state-change", action="store_true", help="Filter functions that change state." ) - parser.add_argument( - "--contains-asm", - action="store_true", - help="If set, filter functions that contain inline assembly.", + "--read-only", action="store_true", help="Filter functions that do not change state." ) - parser.add_argument( - "--low-lvl-calls", - action="store_true", - help="If set, filter functions that make low level calls.", + "--contains-asm", action="store_true", help="Filter functions that contain inline assembly." ) - parser.add_argument( - "--full-name", - type=str, - help="If set, filter functions by their full name.", + "--low-lvl-calls", action="store_true", help="Filter functions that make low level calls." ) - + parser.add_argument("--full-name", type=str, help="Filter functions by their full name.") parser.add_argument( - "--in-source", - type=str, - help="If set, filter functions by the string in their source.", + "--in-source", type=str, help="Filter functions by the string in their source." ) cryticparser.init(parser) @@ -95,106 +63,43 @@ def parse_args() -> Namespace: def filter_function(function: Function, args) -> bool: - # Check visibility - if args.visibility and function.visibility != args.visibility: - return False - - # Check for existence of modifiers - if args.modifiers: - if not function.modifiers: - return False - - # Check for existence of external calls - if args.ext_calls: - if not function.high_level_calls: - return False - - # Check for existence of internal calls - if args.int_calls: - if not function.internal_calls: - return False - - # Check if function potentially changes state - if args.state_change: - if function.view or function.pure: - return False - - # Check if function is read-only - if args.read_only: - if not (function.view or function.pure): - return False - - # Check for existence of inline assembly - if args.contains_asm: - if not function.contains_asm: - return False - - # Check for existence of low level calls - if args.low_lvl_calls: - if not function.low_level_calls: - return False - - # Check for specific function name - if args.full_name: - if args.full_name not in function.full_name: - return False - - # Check for specific string in function source - if args.in_source: - if args.in_source not in function.source_mapping.content: - return False - - # If none of the conditions have returned False, the function matches all provided criteria - return True - - -def main() -> None: + checks = [ + args.visibility and function.visibility != args.visibility, + args.modifiers and not function.modifiers, + args.ext_calls and not function.high_level_calls, + args.int_calls and not function.internal_calls, + args.state_change and (function.view or function.pure), + args.read_only and not (function.view or function.pure), + args.contains_asm and not function.contains_asm, + args.low_lvl_calls and not function.low_level_calls, + args.full_name and args.full_name not in function.full_name, + args.in_source and args.in_source not in function.source_mapping.content, + ] + return not any(checks) + + +def process_contract(contract, args): + target_functions = contract.functions_declared if args.declared_only else contract.functions + results = [] + for function in target_functions: + if filter_function(function, args): + lines = function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() + summary = function.get_summary() + (lines,) + results.append(summary) + return results + + +def main(): args = parse_args() - - # Perform slither analysis on the given filename slither = Slither(args.filename, **vars(args)) - - # Access the arguments - contract_name = args.contract_name - # Store list filter_results = [] for contract in slither.contracts: - # Scan only target contract's functions (declared only or immediate inheritance) - if contract_name: - # Match --contract-name with slither's contract object - if contract.name == contract_name: - # Only target contract's declared functions are scanned - if args.declared_only: - for function in contract.functions_declared: - if filter_function(function, args): - lines = ( - function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() - ) - print(lines) - summary = function.get_summary() + (lines,) - filter_results.append(summary) - - # All functions (declared and inherited) of target contract are scanned - else: - for function in contract.functions: - if filter_function(function, args): - lines = ( - function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() - ) - summary = function.get_summary() + (lines,) - filter_results.append(summary) - - # Scan all contracts in the SourceMapping of filename provided - else: - for function in contract.functions: - if filter_function(function, args): - lines = function.source_mapping.to_detailed_str().rsplit("(", 1)[0].strip() - summary = function.get_summary() + (lines,) - filter_results.append(summary) + if not args.contract_name or contract.name == args.contract_name: + filter_results.extend(process_contract(contract, args)) if filter_results: - logger.info(green(f"Found {len(filter_results)} functions matching flags\n")) + logger.info(f"Found {len(filter_results)} functions matching flags\n") for result in filter_results: ( contract_name, @@ -208,7 +113,6 @@ def main() -> None: cyclomatic_complexity, lines, ) = result - logger.info(bold(f"Function: {contract_name}.{function_sig}")) logger.info(bold(f"Reference: {lines}")) logger.info(blue(f"Visibility: {visibility}")) @@ -219,7 +123,7 @@ def main() -> None: logger.info(blue(f"External Calls: {external_calls}")) logger.info(blue(f"Cyclomatic Complexity: {cyclomatic_complexity}\n")) else: - logger.info(red("No functions matching flags found.")) + logger.info("No functions matching flags found.") if __name__ == "__main__": From 42abe79f803fa7da8a8fe7c4bbb6f755b707792e Mon Sep 17 00:00:00 2001 From: shortdoom Date: Thu, 11 Apr 2024 22:41:43 +0200 Subject: [PATCH 13/14] fix: contains_asm typo --- slither/tools/function_filter/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index d7bc1c9aac..707abfa9ab 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -70,7 +70,7 @@ def filter_function(function: Function, args) -> bool: args.int_calls and not function.internal_calls, args.state_change and (function.view or function.pure), args.read_only and not (function.view or function.pure), - args.contains_asm and not function.contains_asm, + args.contains_asm and not function.contains_assembly, args.low_lvl_calls and not function.low_level_calls, args.full_name and args.full_name not in function.full_name, args.in_source and args.in_source not in function.source_mapping.content, From 094fed85f0ab4610b855fa0d0dec0918d9a63358 Mon Sep 17 00:00:00 2001 From: shortdoom Date: Sat, 13 Apr 2024 00:18:31 +0200 Subject: [PATCH 14/14] chore: add usage info (for cli) --- slither/tools/function_filter/__main__.py | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/slither/tools/function_filter/__main__.py b/slither/tools/function_filter/__main__.py index 707abfa9ab..745d53e22a 100644 --- a/slither/tools/function_filter/__main__.py +++ b/slither/tools/function_filter/__main__.py @@ -14,12 +14,29 @@ def parse_args() -> Namespace: parser = ArgumentParser( description="Return contract functions based on the provided criteria.", - usage="slither-function-filter filename [options]", + usage=""" + Usage: slither-function-filter filename [flags] + + filename: The file path of the contract to be analyzed. + flags: Flag (or string input) to return matching functions. + + Flags: + --contract-name (str): Declared and inherited in the specified contract. + --declared-only (bool): Only declared in the --contract-name. + --visibility (str): Visibility of the functions. + --modifiers (bool): Have modifiers. + --ext-calls (bool): Make external calls. + --int-calls (bool): Make internal calls. + --state-change (bool): Change state. + --read-only (bool): Do not change state. + --contains-asm (bool): Contains inline assembly. + --low-lvl-calls (bool): Make low level calls. + --full-name (str): By their full name. + --in-source (str): By the string in their source (use escape characters). + """, ) - parser.add_argument( - "filename", help="The filename of the contract or truffle directory to analyze." - ) + parser.add_argument("filename", help="The file path of the contract to be analyzed.") parser.add_argument( "--contract-name", type=str,