diff --git a/scripts/ci_test.sh b/scripts/ci_test.sh index 6696a4e890..5e36eee54b 100755 --- a/scripts/ci_test.sh +++ b/scripts/ci_test.sh @@ -13,7 +13,7 @@ test_slither(){ expected="$DIR/../tests/expected_json/$(basename "$1" .sol).$2.json" # run slither detector on input file and save output as json - if ! slither "$1" --solc-disable-warnings --detect "$2" --json "$DIR/tmp-test.json"; + if ! slither --json "$DIR/tmp-test.json" detect "$1" --solc-disable-warnings --detect "$2"; then echo "Slither crashed" exit 255 @@ -39,7 +39,7 @@ test_slither(){ fi # run slither detector on input file and save output as json - if ! slither "$1" --solc-disable-warnings --detect "$2" --legacy-ast --json "$DIR/tmp-test.json"; + if ! slither --json "$DIR/tmp-test.json" detect "$1" --solc-disable-warnings --detect "$2" --legacy-ast; then echo "Slither crashed" exit 255 @@ -75,7 +75,7 @@ generate_expected_json(){ output_filename_txt="$DIR/../tests/expected_json/$(basename "$1" .sol).$2.txt" # run slither detector on input file and save output as json - slither "$1" --solc-disable-warnings --detect "$2" --json "$output_filename" > "$output_filename_txt" 2>&1 + slither --json "$output_filename" detect "$1" --solc-disable-warnings --detect "$2" > "$output_filename_txt" 2>&1 sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$output_filename" -i diff --git a/scripts/ci_test_cli.sh b/scripts/ci_test_cli.sh index 08d9de836b..877818c147 100755 --- a/scripts/ci_test_cli.sh +++ b/scripts/ci_test_cli.sh @@ -4,17 +4,17 @@ solc-select use 0.7.0 -if ! slither "tests/e2e/config/test_json_config/test.sol" --solc-ast --no-fail-pedantic; then +if ! slither detect "tests/e2e/config/test_json_config/test.sol" --solc-ast --no-fail-pedantic; then echo "--solc-ast failed" exit 1 fi -if ! slither "tests/e2e/config/test_json_config/test.sol" --solc-disable-warnings --no-fail-pedantic; then +if ! slither detect "tests/e2e/config/test_json_config/test.sol" --solc-disable-warnings --no-fail-pedantic; then echo "--solc-disable-warnings failed" exit 1 fi -if ! slither "tests/e2e/config/test_json_config/test.sol" --disable-color --no-fail-pedantic; then +if ! slither detect "tests/e2e/config/test_json_config/test.sol" --disable-color --no-fail-pedantic; then echo "--disable-color failed" exit 1 fi diff --git a/scripts/ci_test_dapp.sh b/scripts/ci_test_dapp.sh index b11ea46a55..a240fcb0ca 100755 --- a/scripts/ci_test_dapp.sh +++ b/scripts/ci_test_dapp.sh @@ -20,7 +20,7 @@ nix-env -f "$HOME/.dapp/dapptools" -iA dapp seth solc hevm ethsign dapp init -if ! slither . --detect external-function; then +if ! slither detect . --detect external-function; then echo "Dapp test failed" exit 1 fi diff --git a/scripts/ci_test_embark.sh b/scripts/ci_test_embark.sh index 4bcb6f78c3..49d6562035 100755 --- a/scripts/ci_test_embark.sh +++ b/scripts/ci_test_embark.sh @@ -16,7 +16,7 @@ embark demo cd embark_demo || exit 255 npm install -if ! slither . --embark-overwrite-config; then +if ! slither detect . --embark-overwrite-config; then echo "Embark test failed" exit 255 fi diff --git a/scripts/ci_test_erc.sh b/scripts/ci_test_erc.sh index ebc59475a2..cc5c145130 100755 --- a/scripts/ci_test_erc.sh +++ b/scripts/ci_test_erc.sh @@ -2,14 +2,14 @@ ### Test slither-check-erc -DIR_TESTS="tests/tools/check-erc" +DIR_TESTS="tests/tools/check_erc" solc-select use 0.5.0 -slither-check-erc "$DIR_TESTS/erc20.sol" ERC20 > test_1.txt 2>&1 +slither check-erc "$DIR_TESTS/erc20.sol" ERC20 > test_1.txt 2>&1 DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt") if [ "$DIFF" != "" ] then - echo "slither-check-erc 1 failed" + echo "slither check-erc 1 failed" cat test_1.txt echo "" cat "$DIR_TESTS/test_1.txt" diff --git a/scripts/ci_test_etherlime.sh b/scripts/ci_test_etherlime.sh index 53d3d33501..9c79df6d90 100755 --- a/scripts/ci_test_etherlime.sh +++ b/scripts/ci_test_etherlime.sh @@ -14,7 +14,7 @@ nvm use 10.17.0 npm i -g etherlime etherlime init -if ! slither .; then +if ! slither detect .; then echo "Etherlime test failed" exit 1 fi diff --git a/scripts/ci_test_etherscan.sh b/scripts/ci_test_etherscan.sh index 366b0283d5..7fff29ab03 100755 --- a/scripts/ci_test_etherscan.sh +++ b/scripts/ci_test_etherscan.sh @@ -6,7 +6,7 @@ mkdir etherscan cd etherscan || exit 255 echo "::group::Etherscan mainnet" -if ! slither 0x7F37f78cBD74481E593F9C737776F7113d76B315 --etherscan-apikey "$GITHUB_ETHERSCAN" --no-fail-pedantic; then +if ! slither detect 0x7F37f78cBD74481E593F9C737776F7113d76B315 --etherscan-apikey "$GITHUB_ETHERSCAN" --no-fail-pedantic; then echo "Etherscan mainnet test failed" exit 1 fi diff --git a/scripts/ci_test_find_paths.sh b/scripts/ci_test_find_paths.sh index 1c3652745e..c1cfe8d9a6 100755 --- a/scripts/ci_test_find_paths.sh +++ b/scripts/ci_test_find_paths.sh @@ -5,11 +5,11 @@ DIR_TESTS="tests/possible_paths" solc-select use "0.5.0" -slither-find-paths "$DIR_TESTS/paths.sol" A.destination > test_possible_paths.txt 2>&1 +slither find-paths "$DIR_TESTS/paths.sol" A.destination > test_possible_paths.txt 2>&1 DIFF=$(diff test_possible_paths.txt "$DIR_TESTS/paths.txt") if [ "$DIFF" != "" ] then - echo "slither-find-paths failed" + echo "slither find-paths failed" cat test_possible_paths.txt cat "$DIR_TESTS/paths.txt" exit 255 diff --git a/scripts/ci_test_flat.sh b/scripts/ci_test_flat.sh index 0d9185171e..c4594b6167 100755 --- a/scripts/ci_test_flat.sh +++ b/scripts/ci_test_flat.sh @@ -6,8 +6,8 @@ solc-select use 0.8.19 --always-install cd examples/flat || exit 1 -if ! slither-flat b.sol; then - echo "slither-flat failed" +if ! slither flat b.sol; then + echo "slither flat failed" exit 1 fi diff --git a/scripts/ci_test_interface.sh b/scripts/ci_test_interface.sh index de0defee32..aae8d6d121 100755 --- a/scripts/ci_test_interface.sh +++ b/scripts/ci_test_interface.sh @@ -7,11 +7,11 @@ DIR_TESTS="tests/tools/interface" solc-select use 0.8.19 --always-install #Test 1 - Etherscan target -slither-interface WETH9 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 +slither interface WETH9 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 DIFF=$(diff crytic-export/interfaces/IWETH9.sol "$DIR_TESTS/test_1.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 1 failed" + echo "slither interface test 1 failed" cat "crytic-export/interfaces/IWETH9.sol" echo "" cat "$DIR_TESTS/test_1.sol" @@ -20,11 +20,11 @@ fi #Test 2 - Local file target -slither-interface Mock tests/tools/interface/ContractMock.sol +slither interface Mock tests/tools/interface/ContractMock.sol DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_2.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 2 failed" + echo "slither interface test 2 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_2.sol" @@ -33,11 +33,11 @@ fi #Test 3 - unroll structs -slither-interface Mock tests/tools/interface/ContractMock.sol --unroll-structs +slither interface Mock tests/tools/interface/ContractMock.sol --unroll-structs DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_3.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 3 failed" + echo "slither interface test 3 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_3.sol" @@ -45,11 +45,11 @@ then fi #Test 4 - exclude structs -slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-structs +slither interface Mock tests/tools/interface/ContractMock.sol --exclude-structs DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_4.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 4 failed" + echo "slither interface test 4 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_4.sol" @@ -57,11 +57,11 @@ then fi #Test 5 - exclude errors -slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-errors +slither interface Mock tests/tools/interface/ContractMock.sol --exclude-errors DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_5.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 5 failed" + echo "slither interface test 5 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_5.sol" @@ -69,11 +69,11 @@ then fi #Test 6 - exclude enums -slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-enums +slither interface Mock tests/tools/interface/ContractMock.sol --exclude-enums DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_6.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 6 failed" + echo "slither interface test 6 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_6.sol" @@ -81,11 +81,11 @@ then fi #Test 7 - exclude events -slither-interface Mock tests/tools/interface/ContractMock.sol --exclude-events +slither interface Mock tests/tools/interface/ContractMock.sol --exclude-events DIFF=$(diff crytic-export/interfaces/IMock.sol "$DIR_TESTS/test_7.sol" --strip-trailing-cr) if [ "$DIFF" != "" ] then - echo "slither-interface test 7 failed" + echo "slither interface test 7 failed" cat "crytic-export/interfaces/IMock.sol" echo "" cat "$DIR_TESTS/test_7.sol" diff --git a/scripts/ci_test_kspec.sh b/scripts/ci_test_kspec.sh index 3bd827e694..36e4871ae1 100755 --- a/scripts/ci_test_kspec.sh +++ b/scripts/ci_test_kspec.sh @@ -3,11 +3,11 @@ DIR_TESTS="tests/check-kspec" solc-select use "0.5.0" -slither-check-kspec "$DIR_TESTS/safeAdd/safeAdd.sol" "$DIR_TESTS/safeAdd/spec.md" > test_1.txt 2>&1 +slither check-kspec "$DIR_TESTS/safeAdd/safeAdd.sol" "$DIR_TESTS/safeAdd/spec.md" > test_1.txt 2>&1 DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt") if [ "$DIFF" != "" ] then - echo "slither-check-kspec 1 failed" + echo "slither check-kspec 1 failed" cat test_1.txt echo "" cat "$DIR_TESTS/test_1.txt" diff --git a/scripts/ci_test_path_filtering.sh b/scripts/ci_test_path_filtering.sh index d7a2a9833d..49df598bc9 100755 --- a/scripts/ci_test_path_filtering.sh +++ b/scripts/ci_test_path_filtering.sh @@ -3,7 +3,7 @@ ### Test path filtering across POSIX and Windows solc-select use 0.8.0 -slither "tests/e2e/config/test_path_filtering/test_path_filtering.sol" --config "tests/e2e/config/test_path_filtering/slither.config.json" > "output.txt" 2>&1 +slither detect "tests/e2e/config/test_path_filtering/test_path_filtering.sol" --config "tests/e2e/config/test_path_filtering/slither.config.json" > "output.txt" 2>&1 if ! grep -q "0 result(s) found" "output.txt" then diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 3306c134e2..1b05638a3a 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -9,7 +9,7 @@ ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,fun # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do - if ! slither "$file" --print "$ALL_PRINTERS" ; then + if ! slither print "$file" --print "$ALL_PRINTERS" ; then echo "Printer failed" echo "$file" exit 1 @@ -18,7 +18,7 @@ done # Only test 0.8.12 to limit test time for file in *0.8.12-compact.zip; do - if ! slither "$file" --print "declaration" ; then + if ! slither print "$file" --print "declaration" ; then echo "Printer failed" echo "$file" exit 1 @@ -29,6 +29,6 @@ cd ../../.. || exit # Needed for evm printer pip install evm-cfg-builder solc-select use "0.5.1" -if ! slither examples/scripts/test_evm_api.sol --print evm; then +if ! slither print examples/scripts/test_evm_api.sol --print evm; then echo "EVM printer failed" fi diff --git a/scripts/ci_test_prop.sh b/scripts/ci_test_prop.sh index eed4517a49..3cadf60697 100755 --- a/scripts/ci_test_prop.sh +++ b/scripts/ci_test_prop.sh @@ -3,8 +3,8 @@ ### Test slither-prop cd examples/slither-prop || exit 1 -slither-prop . --contract ERC20Buggy +slither prop . --contract ERC20Buggy if [ ! -f contracts/crytic/TestERC20BuggyTransferable.sol ]; then - echo "slither-prop failed" + echo "slither prop failed" return 1 fi diff --git a/scripts/ci_test_simil.sh b/scripts/ci_test_simil.sh index 7ef5117593..e8b0b4010c 100755 --- a/scripts/ci_test_simil.sh +++ b/scripts/ci_test_simil.sh @@ -5,16 +5,16 @@ pip3.8 install pybind11 pip3.8 install https://github.com/facebookresearch/fastText/archive/0.2.0.zip -### Test slither-simil +### Test slither simil solc-select use "0.4.25" DIR_TESTS="tests/simil" -slither-simil info "" --filename $DIR_TESTS/../complex_func.sol --fname Complex.complexExternalWrites > test_1.txt 2>&1 +slither simil info "" --filename $DIR_TESTS/../complex_func.sol --fname Complex.complexExternalWrites > test_1.txt 2>&1 DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt") if [ "$DIFF" != "" ] then - echo "slither-simil failed" + echo "slither simil failed" cat test_1.txt cat "$DIR_TESTS/test_1.txt" exit 255 diff --git a/scripts/ci_test_slither_config.sh b/scripts/ci_test_slither_config.sh index 525eb5f7c5..6991dff21d 100755 --- a/scripts/ci_test_slither_config.sh +++ b/scripts/ci_test_slither_config.sh @@ -2,7 +2,7 @@ ### Test -if ! slither "tests/*.json" --config "tests/config/slither.config.json"; then +if ! slither --config-file "tests/config/slither.config.json" detect "tests/*.json"; then echo "Config failed" exit 1 fi diff --git a/scripts/ci_test_truffle.sh b/scripts/ci_test_truffle.sh index 0246470f83..aa55b10fcc 100755 --- a/scripts/ci_test_truffle.sh +++ b/scripts/ci_test_truffle.sh @@ -14,7 +14,7 @@ nvm use --lts npm install -g truffle truffle unbox metacoin -if ! slither . --no-fail-pedantic; then +if ! slither detect . --no-fail-pedantic; then echo "Truffle test failed" exit 1 fi diff --git a/scripts/ci_test_upgradability.sh b/scripts/ci_test_upgradability.sh index 0a0d77f519..e69821c143 100755 --- a/scripts/ci_test_upgradability.sh +++ b/scripts/ci_test_upgradability.sh @@ -1,59 +1,59 @@ #!/usr/bin/env bash -### Test slither-check-upgradeability +### Test slither check-upgradeability DIR_TESTS="tests/check-upgradeability" solc-select use "0.5.0" -slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_1.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_1.txt 2>&1 DIFF=$(diff test_1.txt "$DIR_TESTS/test_1.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 1 failed" + echo "slither check-upgradeability 1 failed" cat test_1.txt echo "" cat "$DIR_TESTS/test_1.txt" exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2.sol" --new-contract-name ContractV2 > test_2.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2.sol" --new-contract-name ContractV2 > test_2.txt 2>&1 DIFF=$(diff test_2.txt "$DIR_TESTS/test_2.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 2 failed" + echo "slither check-upgradeability 2 failed" cat test_2.txt echo "" cat "$DIR_TESTS/test_2.txt" exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2_bug.sol" --new-contract-name ContractV2 > test_3.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2_bug.sol" --new-contract-name ContractV2 > test_3.txt 2>&1 DIFF=$(diff test_3.txt "$DIR_TESTS/test_3.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 3 failed" + echo "slither check-upgradeability 3 failed" cat test_3.txt echo "" cat "$DIR_TESTS/test_3.txt" exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2_bug2.sol" --new-contract-name ContractV2 > test_4.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy --new-contract-filename "$DIR_TESTS/contractV2_bug2.sol" --new-contract-name ContractV2 > test_4.txt 2>&1 DIFF=$(diff test_4.txt "$DIR_TESTS/test_4.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 4 failed" + echo "slither check-upgradeability 4 failed" cat test_4.txt echo "" cat "$DIR_TESTS/test_4.txt" exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_5.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_5.txt 2>&1 DIFF=$(diff test_5.txt "$DIR_TESTS/test_5.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 5 failed" + echo "slither check-upgradeability 5 failed" cat test_5.txt echo "" cat "$DIR_TESTS/test_5.txt" @@ -62,11 +62,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_5.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_5.txt 2>&1 DIFF=$(diff test_5.txt "$DIR_TESTS/test_5.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 5 failed" + echo "slither check-upgradeability 5 failed" cat test_5.txt echo "" cat "$DIR_TESTS/test_5.txt" @@ -76,11 +76,11 @@ then fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_lack_to_call_modifier --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_6.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_lack_to_call_modifier --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_6.txt 2>&1 DIFF=$(diff test_6.txt "$DIR_TESTS/test_6.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 6 failed" + echo "slither check-upgradeability 6 failed" cat test_6.txt echo "" cat "$DIR_TESTS/test_6.txt" @@ -90,11 +90,11 @@ then fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_not_called_super_init --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_7.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_not_called_super_init --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_7.txt 2>&1 DIFF=$(diff test_7.txt "$DIR_TESTS/test_7.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 7 failed" + echo "slither check-upgradeability 7 failed" cat test_7.txt echo "" cat "$DIR_TESTS/test_7.txt" @@ -103,11 +103,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug_inherits --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_8.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_no_bug_inherits --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_8.txt 2>&1 DIFF=$(diff test_8.txt "$DIR_TESTS/test_8.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 8 failed" + echo "slither check-upgradeability 8 failed" cat test_8.txt echo "" cat "$DIR_TESTS/test_8.txt" @@ -116,11 +116,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_double_call --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_9.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_initialization.sol" Contract_double_call --proxy-filename "$DIR_TESTS/proxy.sol" --proxy-name Proxy > test_9.txt 2>&1 DIFF=$(diff test_9.txt "$DIR_TESTS/test_9.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 9 failed" + echo "slither check-upgradeability 9 failed" cat test_9.txt echo "" cat "$DIR_TESTS/test_9.txt" @@ -129,11 +129,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contract_v2_constant.sol" --new-contract-name ContractV2 > test_10.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contract_v2_constant.sol" --new-contract-name ContractV2 > test_10.txt 2>&1 DIFF=$(diff test_10.txt "$DIR_TESTS/test_10.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 10 failed" + echo "slither check-upgradeability 10 failed" cat test_10.txt echo "" cat "$DIR_TESTS/test_10.txt" @@ -142,11 +142,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contract_v1_var_init.sol" ContractV1 > test_11.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contract_v1_var_init.sol" ContractV1 > test_11.txt 2>&1 DIFF=$(diff test_11.txt "$DIR_TESTS/test_11.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 11 failed" + echo "slither check-upgradeability 11 failed" cat test_11.txt echo "" cat "$DIR_TESTS/test_11.txt" @@ -155,11 +155,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1_struct.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contractV2_struct.sol" --new-contract-name ContractV2 > test_12.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1_struct.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contractV2_struct.sol" --new-contract-name ContractV2 > test_12.txt 2>&1 DIFF=$(diff test_12.txt "$DIR_TESTS/test_12.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 12 failed" + echo "slither check-upgradeability 12 failed" cat test_12.txt echo "" cat "$DIR_TESTS/test_12.txt" @@ -168,11 +168,11 @@ then exit 255 fi -slither-check-upgradeability "$DIR_TESTS/contractV1_struct.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contractV2_struct_bug.sol" --new-contract-name ContractV2 > test_13.txt 2>&1 +slither check-upgradeability "$DIR_TESTS/contractV1_struct.sol" ContractV1 --new-contract-filename "$DIR_TESTS/contractV2_struct_bug.sol" --new-contract-name ContractV2 > test_13.txt 2>&1 DIFF=$(diff test_13.txt "$DIR_TESTS/test_13.txt") if [ "$DIFF" != "" ] then - echo "slither-check-upgradeability 13 failed" + echo "slither check-upgradeability 13 failed" cat test_13.txt echo "" cat "$DIR_TESTS/test_13.txt" diff --git a/setup.py b/setup.py index 873edd3fe2..733bbab14a 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ "eth-abi>=4.0.0", "eth-typing>=3.0.0", "eth-utils>=2.1.0", + "typer>=0.12.3", ], extras_require={ "lint": [ @@ -50,20 +51,7 @@ long_description_content_type="text/markdown", entry_points={ "console_scripts": [ - "slither = slither.__main__:main", - "slither-check-upgradeability = slither.tools.upgradeability.__main__:main", - "slither-find-paths = slither.tools.possible_paths.__main__:main", - "slither-simil = slither.tools.similarity.__main__:main", - "slither-flat = slither.tools.flattening.__main__:main", - "slither-format = slither.tools.slither_format.__main__:main", - "slither-check-erc = slither.tools.erc_conformance.__main__:main", - "slither-check-kspec = slither.tools.kspec_coverage.__main__:main", - "slither-prop = slither.tools.properties.__main__:main", - "slither-mutate = slither.tools.mutator.__main__:main", - "slither-read-storage = slither.tools.read_storage.__main__:main", - "slither-doctor = slither.tools.doctor.__main__:main", - "slither-documentation = slither.tools.documentation.__main__:main", - "slither-interface = slither.tools.interface.__main__:main", + "slither = slither.__main__:app", ] }, ) diff --git a/slither/__main__.py b/slither/__main__.py index caaef5730b..afdd054aaf 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -1,53 +1,63 @@ #!/usr/bin/env python3 - -import argparse import cProfile import glob import inspect import json import logging import os -import pstats import sys +import textwrap import traceback +from functools import lru_cache from importlib import metadata -from typing import Tuple, Optional, List, Dict, Type, Union, Any, Sequence +from pathlib import Path +from typing import Tuple, Optional, List, Dict, Type, Union, Any +import warnings + +# pylint: disable=wrong-import-position +# We want to disable the warnings thrown by any package when using the completion +if os.environ.get("_TYPER_COMPLETE_ARGS", False): + warnings.filterwarnings("ignore") +import typer +from typing_extensions import Annotated -from crytic_compile import cryticparser, CryticCompile -from crytic_compile.platform.standard import generate_standard_export -from crytic_compile.platform.etherscan import SUPPORTED_NETWORK +from crytic_compile import CryticCompile, InvalidCompilation from crytic_compile import compile_all, is_supported from slither.detectors import all_detectors -from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification +from slither.detectors.abstract_detector import AbstractDetector +from slither.detectors.classification import DetectorClassification from slither.printers import all_printers from slither.printers.abstract_printer import AbstractPrinter from slither.slither import Slither -from slither.utils import codex from slither.utils.output import ( - output_to_json, - output_to_zip, - output_to_sarif, - ZIP_TYPES_ACCEPTED, Output, + ZipType, + OutputFormat, + format_output, + output_to_markdown, + output_wiki, + output_detectors, + output_detectors_json, + output_printers, ) from slither.utils.output_capture import StandardOutputCapture from slither.utils.colors import red, set_colorization_enabled from slither.utils.command_line import ( FailOnLevel, - output_detectors, - output_results_to_markdown, - output_detectors_json, - output_printers, - output_printers_json, - output_to_markdown, - output_wiki, defaults_flag_in_config, - read_config_file, - JSON_OUTPUT_TYPES, DEFAULT_JSON_OUTPUT_TYPES, - check_and_sanitize_markdown_root, + SlitherApp, + slither_end_callback, + SlitherState, + version_callback, + MarkdownRoot, + CommaSeparatedValueParser, + long_help, + Target, + target_type, + read_config_file, ) from slither.exceptions import SlitherException @@ -55,6 +65,11 @@ logger = logging.getLogger("Slither") +app = SlitherApp("detect", rich_markup_mode="markdown", result_callback=slither_end_callback) + +# Because the app will be used by the tools to add commands, we need to define it before importing them +import slither.tools # pylint: disable=unused-import,wrong-import-position + ################################################################################### ################################################################################### # region Process functions @@ -64,7 +79,7 @@ def process_single( target: Union[str, CryticCompile], - args: argparse.Namespace, + state: Dict[str, Any], detector_classes: List[Type[AbstractDetector]], printer_classes: List[Type[AbstractPrinter]], ) -> Tuple[Slither, List[Dict], List[Output], int]: @@ -74,40 +89,44 @@ def process_single( Returns: list(result), int: Result list and number of contracts analyzed """ - ast = "--ast-compact-json" - if args.legacy_ast: - ast = "--ast-json" - slither = Slither(target, ast_format=ast, **vars(args)) + ast = "--ast-compact-json" if not state.get("legacy_ast", False) else "--ast-json" + slither_ = Slither(target, ast_format=ast, **state) - if args.sarif_input: - slither.sarif_input = args.sarif_input - if args.sarif_triage: - slither.sarif_triage = args.sarif_triage + if state.get("sarif_input"): + slither_.sarif_input = state.get("sarif_input") + if state.get("sarif_triage"): + slither_.sarif_triage = state.get("sarif_triage") - return _process(slither, detector_classes, printer_classes) + return _process(slither_, detector_classes, printer_classes) def process_all( target: str, - args: argparse.Namespace, + state: Dict, detector_classes: List[Type[AbstractDetector]], printer_classes: List[Type[AbstractPrinter]], ) -> Tuple[List[Slither], List[Dict], List[Output], int]: - compilations = compile_all(target, **vars(args)) + + try: + compilations = compile_all(target, **state) + except InvalidCompilation: + logger.error("Unable to compile all targets.") + raise typer.Exit(code=2) + slither_instances = [] results_detectors = [] results_printers = [] analyzed_contracts_count = 0 for compilation in compilations: ( - slither, + slither_, current_results_detectors, current_results_printers, current_analyzed_count, - ) = process_single(compilation, args, detector_classes, printer_classes) + ) = process_single(compilation, state, detector_classes, printer_classes) results_detectors.extend(current_results_detectors) results_printers.extend(current_results_printers) - slither_instances.append(slither) + slither_instances.append(slither_) analyzed_contracts_count += current_analyzed_count return ( slither_instances, @@ -118,33 +137,33 @@ def process_all( def _process( - slither: Slither, + slither_: Slither, detector_classes: List[Type[AbstractDetector]], printer_classes: List[Type[AbstractPrinter]], ) -> Tuple[Slither, List[Dict], List[Output], int]: for detector_cls in detector_classes: - slither.register_detector(detector_cls) + slither_.register_detector(detector_cls) for printer_cls in printer_classes: - slither.register_printer(printer_cls) + slither_.register_printer(printer_cls) - analyzed_contracts_count = len(slither.contracts) + analyzed_contracts_count = len(slither_.contracts) results_detectors = [] results_printers = [] if not printer_classes: - detector_resultss = slither.run_detectors() + detector_resultss = slither_.run_detectors() detector_resultss = [x for x in detector_resultss if x] # remove empty results detector_results = [item for sublist in detector_resultss for item in sublist] # flatten results_detectors.extend(detector_results) else: - printer_results = slither.run_printers() + printer_results = slither_.run_printers() printer_results = [x for x in printer_results if x] # remove empty results results_printers.extend(printer_results) - return slither, results_detectors, results_printers, analyzed_contracts_count + return slither_, results_detectors, results_printers, analyzed_contracts_count # endregion @@ -156,6 +175,7 @@ def _process( ################################################################################### +@lru_cache def get_detectors_and_printers() -> Tuple[ List[Type[AbstractDetector]], List[Type[AbstractPrinter]] ]: @@ -178,14 +198,21 @@ def get_detectors_and_printers() -> Tuple[ plugin_detectors, plugin_printers = make_plugin() - detector = None - if not all(issubclass(detector, AbstractDetector) for detector in plugin_detectors): + not_detectors = { + detector for detector in plugin_detectors if not issubclass(detector, AbstractDetector) + } + if not_detectors: raise ValueError( - f"Error when loading plugin {entry_point}, {detector} is not a detector" + f"Error when loading plugin {entry_point}, {not_detectors} are not detectors" + ) + + not_printers = { + printer for printer in plugin_printers if not issubclass(printer, AbstractPrinter) + } + if not_printers: + raise ValueError( + f"Error when loading plugin {entry_point}, {not_printers} are not printers" ) - printer = None - if not all(issubclass(printer, AbstractPrinter) for printer in plugin_printers): - raise ValueError(f"Error when loading plugin {entry_point}, {printer} is not a printer") # We convert those to lists in case someone returns a tuple detectors += list(plugin_detectors) @@ -194,52 +221,60 @@ def get_detectors_and_printers() -> Tuple[ return detectors, printers -# pylint: disable=too-many-branches +# pylint: disable=too-many-branches,too-many-arguments def choose_detectors( - args: argparse.Namespace, all_detector_classes: List[Type[AbstractDetector]] + arg_detector_to_run: str, + arg_detector_exclude: str, + exclude_low: bool = False, + exclude_medium: bool = False, + exclude_high: bool = False, + exclude_optimization: bool = False, + exclude_informational: bool = False, ) -> List[Type[AbstractDetector]]: - # If detectors are specified, run only these ones + # If detectors are specified, run only these + + all_detector_classes: List[Type[AbstractDetector]] = DETECTORS + if all_detector_classes is None: + return [] detectors_to_run = [] - detectors = {d.ARGUMENT: d for d in all_detector_classes} + local_detectors = {d.ARGUMENT: d for d in all_detector_classes} - if args.detectors_to_run == "all": + if arg_detector_to_run == "all": detectors_to_run = all_detector_classes - if args.detectors_to_exclude: - detectors_excluded = args.detectors_to_exclude.split(",") - for detector in detectors: + if arg_detector_exclude: + detectors_excluded = arg_detector_exclude.split(",") + for detector in local_detectors: if detector in detectors_excluded: - detectors_to_run.remove(detectors[detector]) + detectors_to_run.remove(local_detectors[detector]) else: - for detector in args.detectors_to_run.split(","): - if detector in detectors: - detectors_to_run.append(detectors[detector]) + for detector in arg_detector_to_run.split(","): + if detector in local_detectors: + detectors_to_run.append(local_detectors[detector]) else: raise ValueError(f"Error: {detector} is not a detector") detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT) return detectors_to_run - if args.exclude_optimization: + if exclude_optimization: detectors_to_run = [ d for d in detectors_to_run if d.IMPACT != DetectorClassification.OPTIMIZATION ] - if args.exclude_informational: + if exclude_informational: detectors_to_run = [ d for d in detectors_to_run if d.IMPACT != DetectorClassification.INFORMATIONAL ] - if args.exclude_low: + if exclude_low: detectors_to_run = [d for d in detectors_to_run if d.IMPACT != DetectorClassification.LOW] - if args.exclude_medium: + if exclude_medium: detectors_to_run = [ d for d in detectors_to_run if d.IMPACT != DetectorClassification.MEDIUM ] - if args.exclude_high: + if exclude_high: detectors_to_run = [d for d in detectors_to_run if d.IMPACT != DetectorClassification.HIGH] - if args.detectors_to_exclude: - detectors_to_run = [ - d for d in detectors_to_run if d.ARGUMENT not in args.detectors_to_exclude - ] + if arg_detector_exclude: + detectors_to_run = [d for d in detectors_to_run if d.ARGUMENT not in arg_detector_exclude] detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT) @@ -247,19 +282,19 @@ def choose_detectors( def choose_printers( - args: argparse.Namespace, all_printer_classes: List[Type[AbstractPrinter]] + arg_printer_to_run: Union[None, str] = None, ) -> List[Type[AbstractPrinter]]: printers_to_run = [] # disable default printer - if args.printers_to_run is None: + if arg_printer_to_run is None: return [] - if args.printers_to_run == "all": - return all_printer_classes + if arg_printer_to_run == "all": + return PRINTERS - printers = {p.ARGUMENT: p for p in all_printer_classes} - for printer in args.printers_to_run.split(","): + printers = {p.ARGUMENT: p for p in PRINTERS} + for printer in arg_printer_to_run.split(","): if printer in printers: printers_to_run.append(printers[printer]) else: @@ -275,445 +310,56 @@ def choose_printers( ################################################################################### -def parse_filter_paths(args: argparse.Namespace, filter_path: bool) -> List[str]: - paths = args.filter_paths if filter_path else args.include_paths - if paths: - return paths.split(",") - return [] - - -# pylint: disable=too-many-statements -def parse_args( - detector_classes: List[Type[AbstractDetector]], printer_classes: List[Type[AbstractPrinter]] -) -> argparse.Namespace: - usage = "slither target [flag]\n" - usage += "\ntarget can be:\n" - usage += "\t- file.sol // a Solidity file\n" - usage += "\t- project_directory // a project directory. See https://github.com/crytic/crytic-compile/#crytic-compile for the supported platforms\n" - usage += "\t- 0x.. // a contract on mainnet\n" - usage += f"\t- NETWORK:0x.. // a contract on a different network. Supported networks: {','.join(x[:-1] for x in SUPPORTED_NETWORK)}\n" - - parser = argparse.ArgumentParser( - description="For usage information, see https://github.com/crytic/slither/wiki/Usage", - usage=usage, - ) - - parser.add_argument("filename", help=argparse.SUPPRESS) - - cryticparser.init(parser) - - parser.add_argument( - "--version", - help="displays the current version", - version=metadata.version("slither-analyzer"), - action="version", - ) - - group_detector = parser.add_argument_group("Detectors") - group_printer = parser.add_argument_group("Printers") - group_checklist = parser.add_argument_group( - "Checklist (consider using https://github.com/crytic/slither-action)" - ) - group_misc = parser.add_argument_group("Additional options") - group_filters = parser.add_mutually_exclusive_group() - - group_detector.add_argument( - "--detect", - help="Comma-separated list of detectors, defaults to all, " - f"available detectors: {', '.join(d.ARGUMENT for d in detector_classes)}", - action="store", - dest="detectors_to_run", - default=defaults_flag_in_config["detectors_to_run"], - ) - - group_printer.add_argument( - "--print", - help="Comma-separated list of contract information printers, " - f"available printers: {', '.join(d.ARGUMENT for d in printer_classes)}", - action="store", - dest="printers_to_run", - default=defaults_flag_in_config["printers_to_run"], - ) - - group_detector.add_argument( - "--list-detectors", - help="List available detectors", - action=ListDetectors, - nargs=0, - default=False, - ) - - group_printer.add_argument( - "--list-printers", - help="List available printers", - action=ListPrinters, - nargs=0, - default=False, - ) - - group_detector.add_argument( - "--exclude", - help="Comma-separated list of detectors that should be excluded", - action="store", - dest="detectors_to_exclude", - default=defaults_flag_in_config["detectors_to_exclude"], - ) - - group_detector.add_argument( - "--exclude-dependencies", - help="Exclude results that are only related to dependencies", - action="store_true", - default=defaults_flag_in_config["exclude_dependencies"], - ) +def list_detectors_json(ctx: typer.Context, value: bool): + if not value or ctx.resilient_parsing: + return - group_detector.add_argument( - "--exclude-optimization", - help="Exclude optimization analyses", - action="store_true", - default=defaults_flag_in_config["exclude_optimization"], - ) + detector_types_json = output_detectors_json(DETECTORS) + print(json.dumps(detector_types_json)) + raise typer.Exit(code=0) - group_detector.add_argument( - "--exclude-informational", - help="Exclude informational impact analyses", - action="store_true", - default=defaults_flag_in_config["exclude_informational"], - ) - - group_detector.add_argument( - "--exclude-low", - help="Exclude low impact analyses", - action="store_true", - default=defaults_flag_in_config["exclude_low"], - ) - - group_detector.add_argument( - "--exclude-medium", - help="Exclude medium impact analyses", - action="store_true", - default=defaults_flag_in_config["exclude_medium"], - ) - - group_detector.add_argument( - "--exclude-high", - help="Exclude high impact analyses", - action="store_true", - default=defaults_flag_in_config["exclude_high"], - ) - - fail_on_group = group_detector.add_mutually_exclusive_group() - fail_on_group.add_argument( - "--fail-pedantic", - help="Fail if any findings are detected", - action="store_const", - dest="fail_on", - const=FailOnLevel.PEDANTIC, - ) - fail_on_group.add_argument( - "--fail-low", - help="Fail if any low or greater impact findings are detected", - action="store_const", - dest="fail_on", - const=FailOnLevel.LOW, - ) - fail_on_group.add_argument( - "--fail-medium", - help="Fail if any medium or greater impact findings are detected", - action="store_const", - dest="fail_on", - const=FailOnLevel.MEDIUM, - ) - fail_on_group.add_argument( - "--fail-high", - help="Fail if any high impact findings are detected", - action="store_const", - dest="fail_on", - const=FailOnLevel.HIGH, - ) - fail_on_group.add_argument( - "--fail-none", - "--no-fail-pedantic", - help="Do not return the number of findings in the exit code", - action="store_const", - dest="fail_on", - const=FailOnLevel.NONE, - ) - fail_on_group.set_defaults(fail_on=FailOnLevel.PEDANTIC) - - group_detector.add_argument( - "--show-ignored-findings", - help="Show all the findings", - action="store_true", - default=defaults_flag_in_config["show_ignored_findings"], - ) - - group_checklist.add_argument( - "--checklist", - help="Generate a markdown page with the detector results", - action="store_true", - default=False, - ) - - group_checklist.add_argument( - "--checklist-limit", - help="Limit the number of results per detector in the markdown file", - action="store", - default="", - ) - - group_checklist.add_argument( - "--markdown-root", - type=check_and_sanitize_markdown_root, - help="URL for markdown generation", - action="store", - default="", - ) - - group_misc.add_argument( - "--json", - help='Export the results as a JSON file ("--json -" to export to stdout)', - action="store", - default=defaults_flag_in_config["json"], - ) - - group_misc.add_argument( - "--sarif", - help='Export the results as a SARIF JSON file ("--sarif -" to export to stdout)', - action="store", - default=defaults_flag_in_config["sarif"], - ) - - group_misc.add_argument( - "--sarif-input", - help="Sarif input (beta)", - action="store", - default=defaults_flag_in_config["sarif_input"], - ) - - group_misc.add_argument( - "--sarif-triage", - help="Sarif triage (beta)", - action="store", - default=defaults_flag_in_config["sarif_triage"], - ) - - group_misc.add_argument( - "--json-types", - help="Comma-separated list of result types to output to JSON, defaults to " - + f'{",".join(output_type for output_type in DEFAULT_JSON_OUTPUT_TYPES)}. ' - + f'Available types: {",".join(output_type for output_type in JSON_OUTPUT_TYPES)}', - action="store", - default=defaults_flag_in_config["json-types"], - ) - - group_misc.add_argument( - "--zip", - help="Export the results as a zipped JSON file", - action="store", - default=defaults_flag_in_config["zip"], - ) - - group_misc.add_argument( - "--zip-type", - help=f'Zip compression type. One of {",".join(ZIP_TYPES_ACCEPTED.keys())}. Default lzma', - action="store", - default=defaults_flag_in_config["zip_type"], - ) - - group_misc.add_argument( - "--disable-color", - help="Disable output colorization", - action="store_true", - default=defaults_flag_in_config["disable_color"], - ) - - group_misc.add_argument( - "--triage-mode", - help="Run triage mode (save results in triage database)", - action="store_true", - dest="triage_mode", - default=False, - ) - - group_misc.add_argument( - "--triage-database", - help="File path to the triage database (default: slither.db.json)", - action="store", - dest="triage_database", - default=defaults_flag_in_config["triage_database"], - ) - - group_misc.add_argument( - "--config-file", - help="Provide a config file (default: slither.config.json)", - action="store", - dest="config_file", - default=None, - ) - - group_misc.add_argument( - "--change-line-prefix", - help="Change the line prefix (default #) for the displayed source codes (i.e. file.sol#1).", - action="store", - dest="change_line_prefix", - default="#", - ) - - group_misc.add_argument( - "--solc-ast", - help="Provide the contract as a json AST", - action="store_true", - default=False, - ) - - group_misc.add_argument( - "--generate-patches", - help="Generate patches (json output only)", - action="store_true", - default=False, - ) - group_misc.add_argument( - "--no-fail", - help="Do not fail in case of parsing (echidna mode only)", - action="store_true", - default=defaults_flag_in_config["no_fail"], - ) +def list_detectors_action(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return - group_filters.add_argument( - "--filter-paths", - help="Regex filter to exclude detector results matching file path e.g. (mocks/|test/)", - action="store", - dest="filter_paths", - default=defaults_flag_in_config["filter_paths"], - ) + state = ctx.ensure_object(SlitherState) + output_format: OutputFormat = state.get("output_format", OutputFormat.TEXT) + if output_format == OutputFormat.JSON: + StandardOutputCapture.disable() + list_detectors_json(ctx, True) - group_filters.add_argument( - "--include-paths", - help="Regex filter to include detector results matching file path e.g. (src/|contracts/). Opposite of --filter-paths", - action="store", - dest="include_paths", - default=defaults_flag_in_config["include_paths"], - ) + if output_format != OutputFormat.TEXT: + StandardOutputCapture.disable() + logger.error("Unable to output detectors in another format than TEXT or JSON.") + raise typer.Exit(code=1) - codex.init_parser(parser) + output_detectors(DETECTORS) + raise typer.Exit() - # debugger command - parser.add_argument("--debug", help=argparse.SUPPRESS, action="store_true", default=False) - parser.add_argument("--markdown", help=argparse.SUPPRESS, action=OutputMarkdown, default=False) +def list_printers_action(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return - parser.add_argument( - "--wiki-detectors", help=argparse.SUPPRESS, action=OutputWiki, default=False - ) + output_printers(PRINTERS) + raise typer.Exit() - parser.add_argument( - "--list-detectors-json", - help=argparse.SUPPRESS, - action=ListDetectorsJson, - nargs=0, - default=False, - ) - parser.add_argument( - "--legacy-ast", - help=argparse.SUPPRESS, - action="store_true", - default=defaults_flag_in_config["legacy_ast"], - ) +def output_markdown_action(ctx: typer.Context, value: Union[str, None]) -> None: + if value is None or ctx.resilient_parsing: + return - parser.add_argument( - "--skip-assembly", - help=argparse.SUPPRESS, - action="store_true", - default=defaults_flag_in_config["skip_assembly"], - ) + output_to_markdown(DETECTORS, PRINTERS, value) + raise typer.Exit() - parser.add_argument( - "--perf", - help=argparse.SUPPRESS, - action="store_true", - default=False, - ) - # Disable the throw/catch on partial analyses - parser.add_argument( - "--disallow-partial", help=argparse.SUPPRESS, action="store_true", default=False - ) +def output_wiki_action(ctx: typer.Context, _: str, value: Union[str, None] = None): + if ctx.resilient_parsing or value is None: + return - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - args = parser.parse_args() - read_config_file(args) - - args.filter_paths = parse_filter_paths(args, True) - args.include_paths = parse_filter_paths(args, False) - - # Verify our json-type output is valid - args.json_types = set(args.json_types.split(",")) # type:ignore - for json_type in args.json_types: - if json_type not in JSON_OUTPUT_TYPES: - raise ValueError(f'Error: "{json_type}" is not a valid JSON result output type.') - - return args - - -class ListDetectors(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - detectors, _ = get_detectors_and_printers() - output_detectors(detectors) - parser.exit() - - -class ListDetectorsJson(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - detectors, _ = get_detectors_and_printers() - detector_types_json = output_detectors_json(detectors) - print(json.dumps(detector_types_json)) - parser.exit() - - -class ListPrinters(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - _, printers = get_detectors_and_printers() - output_printers(printers) - parser.exit() - - -class OutputMarkdown(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, - parser: Any, - args: Any, - values: Optional[Union[str, Sequence[Any]]], - option_string: Any = None, - ) -> None: - detectors, printers = get_detectors_and_printers() - assert isinstance(values, str) - output_to_markdown(detectors, printers, values) - parser.exit() - - -class OutputWiki(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, - parser: Any, - args: Any, - values: Optional[Union[str, Sequence[Any]]], - option_string: Any = None, - ) -> None: - detectors, _ = get_detectors_and_printers() - assert isinstance(values, str) - output_wiki(detectors, values) - parser.exit() + output_wiki(DETECTORS, value) + raise typer.Exit() # endregion @@ -744,106 +390,461 @@ def format(self, record: logging.LogRecord) -> str: ################################################################################### -def main() -> None: - # Codebase with complex domninators can lead to a lot of SSA recursive call - sys.setrecursionlimit(1500) +# Find all detectors and printers +DETECTORS, PRINTERS = get_detectors_and_printers() - detectors, printers = get_detectors_and_printers() - main_impl(all_detector_classes=detectors, all_printer_classes=printers) +# pylint: disable=unused-argument,too-many-locals +@app.command( + help="Run slither detectors on the target.", +) +def detect( + ctx: typer.Context, + target: target_type, + help_long: Annotated[ + bool, + typer.Option( + "--help-long", + help="Help for crytic compile options.", + is_eager=True, + callback=long_help, + ), + ] = False, + list_json_detector: Annotated[ + Optional[bool], + typer.Option("--list-detectors-json", callback=list_detectors_json, hidden=True), + ] = None, + list_detectors: Annotated[ + Optional[bool], + typer.Option( + "--list-detectors", + help="List available detectors.", + callback=list_detectors_action, + rich_help_panel="Detectors", + is_eager=True, + ), + ] = None, + detectors_to_run: Annotated[ + Optional[str], + typer.Option( + "--detect", + help=f"Comma-separated list of detectors. Available detectors: {', '.join(d.ARGUMENT for d in DETECTORS)}", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["detectors_to_run"], + detectors_to_exclude: Annotated[ + Optional[str], + typer.Option( + "--exclude", + help="Comma-separated list of detectors that should be excluded or all.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["detectors_to_exclude"], + exclude_dependencies: Annotated[ + Optional[bool], + typer.Option( + "--exclude-dependencies", + help="Exclude results that are only related to dependencies.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_dependencies"], + exclude_optimization: Annotated[ + Optional[bool], + typer.Option( + "--exclude-optimization", + help="Exclude optimization analyses.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_optimization"], + exclude_informational: Annotated[ + Optional[bool], + typer.Option( + "--exclude-informational", + help="Exclude informational impact analyses.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_informational"], + exclude_low: Annotated[ + Optional[bool], + typer.Option( + "--exclude-low", + help="Exclude low impact analyses.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_low"], + exclude_medium: Annotated[ + Optional[bool], + typer.Option( + "--exclude-medium", + help="Exclude medium impact analyses.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_medium"], + exclude_high: Annotated[ + Optional[bool], + typer.Option( + "--exclude-high", + help="Exclude high impact analyses.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["exclude_high"], + show_ignored_findings: Annotated[ + Optional[bool], + typer.Option( + "--show-ignored-findings", + help="Show all the findings.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["show_ignored_findings"], + checklist: Annotated[ + Optional[bool], + typer.Option( + "--checklist", + help="Generate a markdown page with the detector results.", + rich_help_panel="Detectors", + ), + ] = False, + checklist_limit: Annotated[ + Optional[int], + typer.Option( + "--checklist-limit", + help="Limit the number of results per detector in the markdown file.", + rich_help_panel="Detectors", + ), + ] = 0, + markdown_root: Annotated[ + Optional[str], + typer.Option( + "--markdown-root", + help="URL for markdown generation.", + rich_help_panel="Detectors", + click_type=MarkdownRoot(), + ), + ] = None, + sarif_input: Annotated[ + Path, + typer.Option( + "--sarif-input", + help="Sarif input (beta).", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["sarif_input"], + sarif_triage: Annotated[ + Path, + typer.Option( + "--sarif-triage", + help="Sarif triage (beta).", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["sarif_triage"], + triage_mode: Annotated[ + bool, + typer.Option( + "--triage-mode", + help="Run triage mode (save results in triage database).", + rich_help_panel="Detectors", + ), + ] = False, + triage_database: Annotated[ + Path, + typer.Option( + "--triage-database", + help="File path to the triage database.", + rich_help_panel="Detectors", + ), + ] = defaults_flag_in_config["triage_database"], + change_line_prefix: Annotated[ + str, + typer.Option( + "--change-line-prefix", + help="Change the line prefix for the displayed source codes (i.e. file.sol:1).", + rich_help_panel="Detectors", + ), + ] = ":", + solc_ast: Annotated[bool, typer.Option(hidden=True)] = False, + json_types: Annotated[ + Optional[str], + typer.Option( + help="Types to include in the JSON output.", click_type=CommaSeparatedValueParser() + ), + ] = ",".join(DEFAULT_JSON_OUTPUT_TYPES), + generate_patches: Annotated[ + bool, + typer.Option( + "--generate-patches", + help="Generate patches (json output only).", + rich_help_panel="Detectors", + ), + ] = False, + fail_pedantic: Annotated[ + bool, + typer.Option( + "--fail-pedantic", + help="Fail if any findings are detected.", + rich_help_panel="Reporting mode", + hidden=True, + ), + ] = False, + fail_low: Annotated[ + bool, + typer.Option( + "--fail-low", + help="Fail if any low or greater impact findings are detected.", + rich_help_panel="Reporting mode", + hidden=True, + ), + ] = False, + fail_medium: Annotated[ + bool, + typer.Option( + "--fail-medium", + help="Fail if any medium or greater impact findings are detected.", + rich_help_panel="Reporting mode", + hidden=True, + ), + ] = False, + fail_high: Annotated[ + bool, + typer.Option( + "--fail-high", + help="Fail if any high or greater impact findings are detected.", + rich_help_panel="Reporting mode", + hidden=True, + ), + ] = False, + fail_none: Annotated[ + bool, + typer.Option( + "--fail-none", + "--no-fail-pedantic", + help="Do not return the number of findings in the exit code.", + rich_help_panel="Reporting mode", + hidden=True, + ), + ] = False, + fail_on: Annotated[ + FailOnLevel, + typer.Option( + "--fail-on", + help=textwrap.dedent( + """ + Fail level: + - *pedantic* : Fail if any findings are detected. + + - *none*: Do not return the number of findings in the exit code. + + - *low*: Fail if any low or greater impact findings are detected. + + - *medium*: Fail if any medium or greater impact findings are detected. + + - *high*: Fail if any high or greater impact findings are detected. + """ + ), + rich_help_panel="Reporting mode", + ), + ] = FailOnLevel.PEDANTIC.value, + # Filtering + filter_paths: Annotated[ + Optional[List[str]], + typer.Option( + "--filter-paths", + help="Regex filter to exclude detector results matching file path e.g. (mocks/|test/).", + click_type=CommaSeparatedValueParser(), + rich_help_panel="Filtering", + ), + ] = defaults_flag_in_config["filter_paths"], + include_paths: Annotated[ + Optional[List[str]], + typer.Option( + "--include-paths", + help="Regex filter to include detector results matching file path e.g. (src/|contracts/).", + click_type=CommaSeparatedValueParser(), + rich_help_panel="Filtering", + ), + ] = defaults_flag_in_config["include_paths"], +): + """Run detectors and report findings.""" + + state = ctx.ensure_object(SlitherState) + # Update the state + state.update( + { + "markdown_root": markdown_root, + "sarif_input": sarif_input, + "sarif_triage": sarif_triage, + "triage_mode": triage_mode, + "triage_database": triage_database, + "change_line_prefix": change_line_prefix, + "generate_patches": generate_patches, + "filter_paths": filter_paths, + "include_paths": include_paths, + } + ) + detector_classes = choose_detectors( + detectors_to_run, + detectors_to_exclude, + exclude_low, + exclude_medium, + exclude_high, + exclude_optimization, + exclude_informational, + ) -# pylint: disable=too-many-statements,too-many-branches,too-many-locals -def main_impl( - all_detector_classes: List[Type[AbstractDetector]], - all_printer_classes: List[Type[AbstractPrinter]], -) -> None: - """ - :param all_detector_classes: A list of all detectors that can be included/excluded. - :param all_printer_classes: A list of all printers that can be included. - """ - # Set logger of Slither to info, to catch warnings related to the arg parsing - logger.setLevel(logging.INFO) - args = parse_args(all_detector_classes, all_printer_classes) - - cp: Optional[cProfile.Profile] = None - if args.perf: - cp = cProfile.Profile() - cp.enable() - - # Set colorization option - set_colorization_enabled(False if args.disable_color else sys.stdout.isatty()) - - # Define some variables for potential JSON output - json_results: Dict[str, Any] = {} - output_error = None - outputting_json = args.json is not None - outputting_json_stdout = args.json == "-" - outputting_sarif = args.sarif is not None - outputting_sarif_stdout = args.sarif == "-" - outputting_zip = args.zip is not None - if args.zip_type not in ZIP_TYPES_ACCEPTED: - to_log = f'Zip type not accepted, it must be one of {",".join(ZIP_TYPES_ACCEPTED.keys())}' - logger.error(to_log) - - # If we are outputting JSON, capture all standard output. If we are outputting to stdout, we block typical stdout - # output. - if outputting_json or outputting_sarif: - StandardOutputCapture.enable(outputting_json_stdout or outputting_sarif_stdout) - - printer_classes = choose_printers(args, all_printer_classes) - detector_classes = choose_detectors(args, all_detector_classes) - - default_log = logging.INFO if not args.debug else logging.DEBUG - - for (l_name, l_level) in [ - ("Slither", default_log), - ("Contract", default_log), - ("Function", default_log), - ("Node", default_log), - ("Parsing", default_log), - ("Detectors", default_log), - ("FunctionSolc", default_log), - ("ExpressionParsing", default_log), - ("TypeParsing", default_log), - ("SSA_Conversion", default_log), - ("Printers", default_log), - # ('CryticCompile', default_log) - ]: - logger_level = logging.getLogger(l_name) - logger_level.setLevel(l_level) + if fail_on == FailOnLevel.PEDANTIC: + fail_levels = {level: locals().get(f"fail_{level.value}") for level in FailOnLevel} + count = list(fail_levels.values()).count(True) + if count == 1: + print("Deprecated way of setting levels.") + fail_on = FailOnLevel([level for level in fail_levels if fail_levels[level]].pop()) + elif count > 1: + raise typer.BadParameter("Only one fail level is allowed.") + else: + fail_on = FailOnLevel.PEDANTIC - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) + slither_instances, results_detectors, _, output_errors, number_contracts = handle_target( + ctx, + target, + solc_ast=solc_ast, + detectors_to_run=detector_classes, + ) - console_handler.setFormatter(FormatterCryticCompile()) + if number_contracts == 0: + logger.warning(red("No contract was analyzed")) + else: + logger.info( + "%s analyzed (%d contracts with %d detectors), %d result(s) found", + target.target, + number_contracts, + len(detector_classes), + len(results_detectors), + ) - crytic_compile_error = logging.getLogger(("CryticCompile")) - crytic_compile_error.addHandler(console_handler) - crytic_compile_error.propagate = False - crytic_compile_error.setLevel(logging.INFO) + format_output( + state.get("output_format"), + state.get("output_file"), + slither_instances, + results_detectors, + [], + output_errors, + runned_detectors=detector_classes, + json_types=json_types, + zip_type=state.get("zip_type"), + checklist=checklist, + checklist_limit=checklist_limit, + show_ignored_findings=show_ignored_findings, + all_detectors=DETECTORS, + ) + + if fail_on == FailOnLevel.HIGH: + fail_on_detection = any(result["impact"] == "High" for result in results_detectors) + elif fail_on == FailOnLevel.MEDIUM: + fail_on_detection = any( + result["impact"] in ["Medium", "High"] for result in results_detectors + ) + elif fail_on == FailOnLevel.LOW: + fail_on_detection = any( + result["impact"] in ["Low", "Medium", "High"] for result in results_detectors + ) + elif fail_on == FailOnLevel.PEDANTIC: + fail_on_detection = bool(results_detectors) + else: + fail_on_detection = False + + # Exit with them appropriate status code + if output_errors or fail_on_detection: + raise typer.Exit(1) + + +@app.command(name="print", help="Run printers on the target.") +def printer_command( + ctx: typer.Context, + target: target_type, + list_printers: Annotated[ + Optional[bool], + typer.Option( + "--list-printers", + help="List available printers.", + callback=list_printers_action, + rich_help_panel="Printers", + is_eager=True, + ), + ] = False, + printers_to_run: Annotated[ + Optional[str], + typer.Option( + "--print", + help="Comma-separated list of contract information printers, " + f"available printers: {', '.join(d.ARGUMENT for d in PRINTERS)}", + rich_help_panel="Printers", + ), + ] = defaults_flag_in_config["printers_to_run"], +): + choosen_printers = choose_printers(printers_to_run) + + slither_instances, _, results_printers, output_errors, number_contracts = handle_target( + ctx, + target, + printers_to_run=choosen_printers, + ) + + state = ctx.ensure_object(SlitherState) + format_output( + output_format=state.get("output_format"), + output_file=state.get("output_file"), + slither_instances=slither_instances, + results_detectors=[], + results_printers=results_printers, + output_error=output_errors, + all_printers=PRINTERS, + ) + + if number_contracts == 0: + logger.warning(red("No contract was analyzed")) + else: + logger.info("%s analyzed (%d contracts)", target.target, number_contracts) + + +# pylint: disable=too-many-locals +def handle_target( + ctx: typer.Context, + target: Target, + solc_ast: bool = False, + detectors_to_run: Optional[List[Type[AbstractDetector]]] = None, + printers_to_run: Optional[List[Type[AbstractPrinter]]] = None, +): + + if detectors_to_run is None: + detectors_to_run = [] + + if printers_to_run is None: + printers_to_run = [] + + state = ctx.ensure_object(SlitherState) results_detectors: List[Dict] = [] results_printers: List[Output] = [] + output_error: Union[str, None] = None + slither_instances = [] + try: - filename = args.filename + filename = target.target - # Determine if we are handling ast from solc - if args.solc_ast or (filename.endswith(".json") and not is_supported(filename)): + if solc_ast or (filename.endswith(".json") and not is_supported(filename)): globbed_filenames = glob.glob(filename, recursive=True) + filenames = glob.glob(os.path.join(filename, "*.json")) if not filenames: filenames = globbed_filenames number_contracts = 0 - slither_instances = [] for filename in filenames: ( slither_instance, results_detectors_tmp, results_printers_tmp, number_contracts_tmp, - ) = process_single(filename, args, detector_classes, printer_classes) + ) = process_single(filename, state, detectors_to_run, printers_to_run) number_contracts += number_contracts_tmp results_detectors += results_detectors_tmp results_printers += results_printers_tmp @@ -856,57 +857,7 @@ def main_impl( results_detectors, results_printers, number_contracts, - ) = process_all(filename, args, detector_classes, printer_classes) - - # Determine if we are outputting JSON - if outputting_json or outputting_zip or output_to_sarif: - # Add our compilation information to JSON - if "compilations" in args.json_types: - compilation_results = [] - for slither_instance in slither_instances: - assert slither_instance.crytic_compile - compilation_results.append( - generate_standard_export(slither_instance.crytic_compile) - ) - json_results["compilations"] = compilation_results - - # Add our detector results to JSON if desired. - if results_detectors and "detectors" in args.json_types: - json_results["detectors"] = results_detectors - - # Add our printer results to JSON if desired. - if results_printers and "printers" in args.json_types: - json_results["printers"] = results_printers - - # Add our detector types to JSON - if "list-detectors" in args.json_types: - detectors, _ = get_detectors_and_printers() - json_results["list-detectors"] = output_detectors_json(detectors) - - # Add our detector types to JSON - if "list-printers" in args.json_types: - _, printers = get_detectors_and_printers() - json_results["list-printers"] = output_printers_json(printers) - - # Output our results to markdown if we wish to compile a checklist. - if args.checklist: - output_results_to_markdown( - results_detectors, args.checklist_limit, args.show_ignored_findings - ) - - # Don't print the number of result for printers - if number_contracts == 0: - logger.warning(red("No contract was analyzed")) - if printer_classes: - logger.info("%s analyzed (%d contracts)", filename, number_contracts) - else: - logger.info( - "%s analyzed (%d contracts with %d detectors), %d result(s) found", - filename, - number_contracts, - len(detector_classes), - len(results_detectors), - ) + ) = process_all(filename, state, detectors_to_run, printers_to_run) except SlitherException as slither_exception: output_error = str(slither_exception) @@ -915,54 +866,198 @@ def main_impl( logging.error(red(output_error)) logging.error("Please report an issue to https://github.com/crytic/slither/issues") - # If we are outputting JSON, capture the redirected output and disable the redirect to output the final JSON. - if outputting_json: - if "console" in args.json_types: - json_results["console"] = { - "stdout": StandardOutputCapture.get_stdout_output(), - "stderr": StandardOutputCapture.get_stderr_output(), - } - StandardOutputCapture.disable() - output_to_json(None if outputting_json_stdout else args.json, output_error, json_results) - - if outputting_sarif: - StandardOutputCapture.disable() - output_to_sarif( - None if outputting_sarif_stdout else args.sarif, json_results, detector_classes - ) - - if outputting_zip: - output_to_zip(args.zip, output_error, json_results, args.zip_type) + return slither_instances, results_detectors, results_printers, output_error, number_contracts + + +@app.callback(invoke_without_command=True, no_args_is_help=True) +def main_callback( + ctx: typer.Context, + version: Annotated[ + Optional[bool], + typer.Option( + "--version", + callback=version_callback, + help="Displays the current version.", + is_eager=True, + ), + ] = None, + output_format: Annotated[ + OutputFormat, + typer.Option("--output-format", help="Output format.", rich_help_panel="Formatting"), + ] = OutputFormat.TEXT, + output_file: Annotated[ + Path, typer.Option("--output-file", help="Output file. Use - for stdout.") + ] = "-", + zip_: Annotated[ + Optional[str], + typer.Option( + "--zip", + hidden=True, + help="Export the results as a zipped JSON file.", + rich_help_panel="Formatting", + ), + ] = None, + json_: Annotated[ + Optional[str], + typer.Option( + "--json", + hidden=True, + help="Print results in JSON format.", + rich_help_panel="Formatting", + ), + ] = None, + sarif: Annotated[ + Optional[str], + typer.Option( + "--sarif", + hidden=True, + help="Print results in SARIF format.", + rich_help_panel="Formatting", + ), + ] = None, + zip_type: Annotated[ + Optional[ZipType], + typer.Option( + "--zip-type", + help="Zip compression type.", + rich_help_panel="Formatting", + ), + ] = defaults_flag_in_config["zip_type"], + disable_color: Annotated[ + bool, + typer.Option( + "--disable-color", + help=f"Disable output colorization. " + f"Implicit if the output format is not {OutputFormat.TEXT.value}.", + rich_help_panel="Formatting", + ), + ] = defaults_flag_in_config["disable_color"], + # Misc + no_fail: Annotated[ + bool, + typer.Option( + "--no-fail", + help="Do not fail in case of parsing (echidna mode only).", + rich_help_panel="Misc", + ), + ] = defaults_flag_in_config["no_fail"], + config_file: Annotated[ + Optional[Path], + typer.Option( + "--config-file", + help="Configuration file. Any argument specified on the command line overrides the one " + "specified in the configuration file.", + rich_help_panel="Misc", + show_default="slither.config.json", + ), + ] = None, + debug: Annotated[bool, typer.Option(hidden=True)] = False, + markdown: Annotated[ + Optional[str], typer.Option(hidden=True, callback=output_markdown_action) + ] = None, + wiki_detectors: Annotated[ + Optional[str], + typer.Option( + help="Print each detectors information that matches the pattern.", + callback=output_wiki_action, + ), + ] = None, + legacy_ast: Annotated[bool, typer.Option(hidden=True)] = False, + skip_assembly: Annotated[bool, typer.Option(hidden=True)] = False, + perf: Annotated[bool, typer.Option(help="Profile slither execution.", hidden=True)] = False, + disallow_partial: Annotated[bool, typer.Option(hidden=True)] = False, +): + """Slither is a static Solidity/Vyper/Yul analyzer. + + Maintained by Trail of Bits. + [https://github.com/crytic/slither](https://github.com/crytic/slither) + """ + state = ctx.ensure_object(SlitherState) + + if perf: + print("PERF ENABLED") + state["perf"] = cProfile.Profile() + state["perf"].enable() + + # Formatting configuration + if zip_ or sarif or json_: + if output_format != OutputFormat.TEXT: + raise typer.BadParameter("Only specify the output format once.") + + options_set = [bool(zip_), bool(sarif), bool(json_)] + if options_set.count(True) > 1: + raise typer.BadParameter("Mutually excluding formatting options set.") + + if zip_ is not None: + output_format = OutputFormat.ZIP + output_file = Path(zip_) + elif sarif is not None: + output_format = OutputFormat.SARIF + output_file = Path(sarif) + elif json_ is not None: + output_format = OutputFormat.JSON + output_file = Path(json_) + + if output_format != OutputFormat.TEXT: + disable_color = True + + set_colorization_enabled(False if disable_color else sys.stdout.isatty()) + + if output_format in (OutputFormat.JSON, OutputFormat.SARIF): + StandardOutputCapture.enable(output_file == Path("-")) + + config = read_config_file(config_file) + + log_level = logging.INFO if not debug else logging.DEBUG + configure_logger(log_level) + + # Update the state with the current + locals_vars = locals() + for option in config: + if option in locals_vars: + state[option] = locals_vars[option] + else: + state[option] = config[option] + + state["output_format"] = output_format + state["output_file"] = output_file + + +def configure_logger(log_level: int = logging.INFO): + """Configure slither loggers.""" + for logger_name in [ + "Slither", + "Contract", + "Function", + "Node", + "Parsing", + "Detectors", + "FunctionSolc", + "ExpressionParsing", + "TypeParsing", + "SSA_Conversion", + "Printers", + ]: + logging.getLogger(logger_name).setLevel(log_level) - if args.perf and cp: - cp.disable() - stats = pstats.Stats(cp).sort_stats("cumtime") - stats.print_stats() + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) - fail_on = FailOnLevel(args.fail_on) - if fail_on == FailOnLevel.HIGH: - fail_on_detection = any(result["impact"] == "High" for result in results_detectors) - elif fail_on == FailOnLevel.MEDIUM: - fail_on_detection = any( - result["impact"] in ["Medium", "High"] for result in results_detectors - ) - elif fail_on == FailOnLevel.LOW: - fail_on_detection = any( - result["impact"] in ["Low", "Medium", "High"] for result in results_detectors - ) - elif fail_on == FailOnLevel.PEDANTIC: - fail_on_detection = bool(results_detectors) - else: - fail_on_detection = False + console_handler.setFormatter(FormatterCryticCompile()) - # Exit with them appropriate status code - if output_error or fail_on_detection: - sys.exit(-1) - else: - sys.exit(0) + crytic_compile_error = logging.getLogger("CryticCompile") + crytic_compile_error.addHandler(console_handler) + crytic_compile_error.propagate = False + crytic_compile_error.setLevel(logging.INFO) if __name__ == "__main__": - main() - -# endregion + pass + # + # + # logger.setLevel(logging.INFO) + # + # # Codebase with complex dominators can lead to a lot of SSA recursive call + # sys.setrecursionlimit(1500) + # + # app() diff --git a/slither/detectors/abstract_detector.py b/slither/detectors/abstract_detector.py index 8baf9bb3c7..d7db523bee 100644 --- a/slither/detectors/abstract_detector.py +++ b/slither/detectors/abstract_detector.py @@ -7,45 +7,23 @@ from slither.core.declarations import Contract from slither.formatters.exceptions import FormatImpossible from slither.formatters.utils.patches import apply_patch, create_diff -from slither.utils.colors import green, yellow, red -from slither.utils.comparable_enum import ComparableEnum -from slither.utils.output import Output, SupportedOutput +from slither.utils.colors import yellow +from slither.detectors.classification import ( + classification_txt, + classification_colors, + DetectorClassification, +) +from slither.utils.output import Output if TYPE_CHECKING: from slither import Slither + from slither.utils.output import SupportedOutput class IncorrectDetectorInitialization(Exception): pass -class DetectorClassification(ComparableEnum): - HIGH = 0 - MEDIUM = 1 - LOW = 2 - INFORMATIONAL = 3 - OPTIMIZATION = 4 - - UNIMPLEMENTED = 999 - - -classification_colors: Dict[DetectorClassification, Callable[[str], str]] = { - DetectorClassification.INFORMATIONAL: green, - DetectorClassification.OPTIMIZATION: green, - DetectorClassification.LOW: green, - DetectorClassification.MEDIUM: yellow, - DetectorClassification.HIGH: red, -} - -classification_txt = { - DetectorClassification.INFORMATIONAL: "Informational", - DetectorClassification.OPTIMIZATION: "Optimization", - DetectorClassification.LOW: "Low", - DetectorClassification.MEDIUM: "Medium", - DetectorClassification.HIGH: "High", -} - - def make_solc_versions(minor: int, patch_min: int, patch_max: int) -> List[str]: """ Create a list of solc version: [0.minor.patch_min .... 0.minor.patch_max] @@ -59,7 +37,7 @@ def make_solc_versions(minor: int, patch_min: int, patch_max: int) -> List[str]: ALL_SOLC_VERSIONS_07 = make_solc_versions(7, 0, 6) # No VERSIONS_08 as it is still in dev -DETECTOR_INFO = List[Union[str, SupportedOutput]] +DETECTOR_INFO = List[Union[str, "SupportedOutput"]] class AbstractDetector(metaclass=abc.ABCMeta): diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index ff1c352c31..d8de744d95 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -87,7 +87,6 @@ from .statements.delegatecall_in_loop import DelegatecallInLoop from .functions.protected_variable import ProtectedVariables from .functions.permit_domain_signature_collision import DomainSeparatorCollision -from .functions.codex import Codex from .functions.cyclomatic_complexity import CyclomaticComplexity from .operations.cache_array_length import CacheArrayLength from .statements.incorrect_using_for import IncorrectUsingFor diff --git a/slither/detectors/classification.py b/slither/detectors/classification.py new file mode 100644 index 0000000000..8785d96ff7 --- /dev/null +++ b/slither/detectors/classification.py @@ -0,0 +1,31 @@ +from typing import Dict, Callable + +from slither.utils.colors import green, yellow, red +from slither.utils.comparable_enum import ComparableEnum + + +class DetectorClassification(ComparableEnum): + HIGH = 0 + MEDIUM = 1 + LOW = 2 + INFORMATIONAL = 3 + OPTIMIZATION = 4 + + UNIMPLEMENTED = 999 + + +classification_colors: Dict[DetectorClassification, Callable[[str], str]] = { + DetectorClassification.INFORMATIONAL: green, + DetectorClassification.OPTIMIZATION: green, + DetectorClassification.LOW: green, + DetectorClassification.MEDIUM: yellow, + DetectorClassification.HIGH: red, +} + +classification_txt = { + DetectorClassification.INFORMATIONAL: "Informational", + DetectorClassification.OPTIMIZATION: "Optimization", + DetectorClassification.LOW: "Low", + DetectorClassification.MEDIUM: "Medium", + DetectorClassification.HIGH: "High", +} diff --git a/slither/slither.py b/slither/slither.py index 0f22185353..4cd475875a 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -177,13 +177,16 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: self._detectors = [] self._printers = [] - filter_paths = kwargs.get("filter_paths", []) - for p in filter_paths: - self.add_path_to_filter(p) - - include_paths = kwargs.get("include_paths", []) - for p in include_paths: - self.add_path_to_include(p) + # The default value for filters_paths / include_paths is None (and not []) + filter_paths: Optional[List[str]] = kwargs.get("filter_paths", None) + if filter_paths is not None: + for p in filter_paths: + self.add_path_to_filter(p) + + include_paths: Optional[List[str]] = kwargs.get("include_paths", None) + if include_paths is not None: + for p in include_paths: + self.add_path_to_include(p) self._exclude_dependencies = kwargs.get("exclude_dependencies", False) diff --git a/slither/tools/__init__.py b/slither/tools/__init__.py index e69de29bb2..618a30c0fe 100644 --- a/slither/tools/__init__.py +++ b/slither/tools/__init__.py @@ -0,0 +1,35 @@ +from slither.tools.demo.__main__ import main as demo_main +from slither.tools.doctor.__main__ import main as doctor_main +from slither.tools.codex.__main__ import codex_callback as codex_main +from slither.tools.erc_conformance.__main__ import main as erc_conformance_main +from slither.tools.flattening.__main__ import main as flattening_main +from slither.tools.interface.__main__ import main as interface_main +from slither.tools.kspec_coverage.__main__ import main as kspec_coverage_main +from slither.tools.mutator.__main__ import main as mutator_main +from slither.tools.possible_paths.__main__ import main as possible_paths_main +from slither.tools.properties.__main__ import main as properties_main +from slither.tools.read_storage.__main__ import main as read_storage_main + +try: + from slither.tools.similarity.__main__ import main as similarity_main +except ImportError: + pass + +from slither.tools.slither_format.__main__ import main as slither_format_main +from slither.tools.upgradeability.__main__ import main as upgradeability_main + +__all__ = [ + "demo_main", + "doctor_main", + "codex_main", + "erc_conformance_main", + "flattening_main", + "interface_main", + "kspec_coverage_main", + "mutator_main", + "possible_paths_main", + "properties_main", + "read_storage_main", + "slither_format_main", + "upgradeability_main", +] diff --git a/slither/tools/documentation/README.md b/slither/tools/codex/README.md similarity index 59% rename from slither/tools/documentation/README.md rename to slither/tools/codex/README.md index b4b3e6a76b..20967cd740 100644 --- a/slither/tools/documentation/README.md +++ b/slither/tools/codex/README.md @@ -1,5 +1,5 @@ # slither-documentation -`slither-documentation` uses [codex](https://beta.openai.com) to generate natspec documenation. +`slither codex documentation` uses [codex](https://beta.openai.com) to generate natspec documenation. This tool is experimental. See [solmate documentation](https://github.com/montyly/solmate/pull/1) for an example of usage. diff --git a/slither/tools/documentation/__init__.py b/slither/tools/codex/__init__.py similarity index 100% rename from slither/tools/documentation/__init__.py rename to slither/tools/codex/__init__.py diff --git a/slither/tools/codex/__main__.py b/slither/tools/codex/__main__.py new file mode 100644 index 0000000000..8614d86747 --- /dev/null +++ b/slither/tools/codex/__main__.py @@ -0,0 +1,156 @@ +import logging +from typing import Annotated, Optional + +import typer + +from slither import Slither +from slither.__main__ import app +from slither.utils.output import format_output +from slither.utils.command_line import ( + target_type, + SlitherState, + SlitherApp, + GroupWithCrytic, + defaults_flag_in_config, +) +from slither.tools.codex.utils import openai_module +from slither.tools.codex.documentation import _handle_compilation_unit +from slither.tools.codex.detector import Codex + + +logging.basicConfig() +logging.getLogger("Slither").setLevel(logging.INFO) + +logger = logging.getLogger("Slither-demo") + + +codex_app = SlitherApp(help="Codex integration.") +app.add_typer(codex_app, name="codex") + + +@codex_app.command() +def documentation( + ctx: typer.Context, + target: target_type, + overwrite: Annotated[bool, typer.Option(help="Overwrite the files (be careful).")] = False, + force_answer_parsing: Annotated[ + bool, + typer.Option( + help="Apply heuristics to better parse codex output (might lead to incorrect results)." + ), + ] = False, + include_tests: Annotated[bool, typer.Option(help="Include the tests.")] = False, + retry: Annotated[ + int, + typer.Option(help="Retry failed query. Each retry increases the temperature by 0.1"), + ] = 1, +): + """Auto-generate NatSpec documentation for every function using OpenAI Codex.""" + + state = ctx.ensure_object(SlitherState) + logger.info("This tool is a WIP, use it with cautious") + logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/") + slither = Slither(target.target, **state) + + try: + for compilation_unit in slither.compilation_units: + _handle_compilation_unit( + slither, + compilation_unit, + overwrite, + force_answer_parsing, + retry, + include_tests, + ) + except ImportError: + pass + + +@codex_app.command() +def detect( + ctx: typer.Context, + target: target_type, +): + state = ctx.ensure_object(SlitherState) + print(f"{state['codex']=}") + slither = Slither(target.target, **state) + + slither.register_detector(Codex) + + print(len(slither.detectors)) + + results = slither.run_detectors() + detector_results = [x for x in results if x] # remove empty results + + format_output( + state.get("output_format"), + state.get("output_file"), + [slither], + detector_results, + [], + "", + runned_detectors=[Codex], + ) + + +@codex_app.callback(cls=GroupWithCrytic) +def codex_callback( + ctx: typer.Context, + api_key: Annotated[ + str, typer.Option("--api-key", help="Open API Key", envvar="OPENAI_API_KEY") + ], + codex_log: Annotated[ + bool, typer.Option("--codex-log", help="Log codex queries (in crytic_export/codex/).") + ] = False, + codex_contracts: Annotated[ + str, + typer.Option( + "--codex-contracts", help="Comma separated list of contracts to submit to OpenAI Codex." + ), + ] = defaults_flag_in_config["codex_contracts"], + codex_model: Annotated[ + str, + typer.Option("--codex-model", help="Name of the Codex model to use (affects pricing)."), + ] = defaults_flag_in_config["codex_model"], + codex_temperature: Annotated[ + int, + typer.Option( + "--codex-temperature", + help="Temperature to use with Codex. Lower number indicates a more precise answer " + "while higher numbers return more creative answers.", + ), + ] = defaults_flag_in_config["codex_temperature"], + codex_max_tokens: Annotated[ + int, + typer.Option( + "--codex-max-tokens", + help="Maximum amount of tokens to use on the response. This number plus the size of " + "the prompt can be no larger than the limit.", + ), + ] = defaults_flag_in_config["codex_max_tokens"], + codex_organization: Annotated[ + Optional[str], + typer.Option("--codex-organization", help="Codex organization."), + ] = None, +): + """Codex (https://beta.openai.com/docs/guides/code).""" + + if openai_module(api_key) is None: + raise typer.Exit(code=1) + + state = ctx.ensure_object(SlitherState) + state.update( + { + "codex_log": codex_log, + "codex_organization": codex_organization, + "codex_model": codex_model, + "codex_temperature": codex_temperature, + "codex_max_tokens": codex_max_tokens, + "codex_contracts": codex_contracts, + "codex": True, + } + ) + + +if __name__ == "__main__": + codex_app() diff --git a/slither/detectors/functions/codex.py b/slither/tools/codex/detector.py similarity index 90% rename from slither/detectors/functions/codex.py rename to slither/tools/codex/detector.py index e58658a3a3..67af77854c 100644 --- a/slither/detectors/functions/codex.py +++ b/slither/tools/codex/detector.py @@ -3,7 +3,8 @@ from typing import List, Union from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification -from slither.utils import codex +from slither.tools.codex.utils import log_codex +from slither.tools.codex.utils import openai_module as find_openai_module from slither.utils.output import Output, SupportedOutput logger = logging.getLogger("Slither") @@ -43,12 +44,12 @@ def _run_codex(self, logging_file: str, prompt: str) -> str: Returns: codex answer (str) """ - openai_module = codex.openai_module() # type: ignore + openai_module = find_openai_module() # type: ignore if openai_module is None: return "" if self.slither.codex_log: - codex.log_codex(logging_file, "Q: " + prompt) + log_codex(logging_file, "Q: " + prompt) answer = "" res = {} @@ -87,11 +88,11 @@ def _run_codex(self, logging_file: str, prompt: str) -> str: # } # } """ - if res: - if self.slither.codex_log: - codex.log_codex(logging_file, "A: " + str(res)) - else: - codex.log_codex(logging_file, "A: Codex failed") + if self.slither.codex_log: + if res: + log_codex(logging_file, "A: " + str(res)) + else: + log_codex(logging_file, "A: Codex failed") if res.get("choices", []) and VULN_FOUND in res["choices"][0].get("text", ""): # remove VULN_FOUND keyword and cleanup diff --git a/slither/tools/documentation/__main__.py b/slither/tools/codex/documentation.py similarity index 72% rename from slither/tools/documentation/__main__.py rename to slither/tools/codex/documentation.py index 0244dd6c67..fe7f00d753 100644 --- a/slither/tools/documentation/__main__.py +++ b/slither/tools/codex/documentation.py @@ -1,64 +1,16 @@ -import argparse import logging import uuid from typing import Optional, Dict, List -from crytic_compile import cryticparser + from slither import Slither from slither.core.compilation_unit import SlitherCompilationUnit from slither.core.declarations import Function - from slither.formatters.utils.patches import create_patch, apply_patch, create_diff -from slither.utils import codex - -logging.basicConfig() -logging.getLogger("Slither").setLevel(logging.INFO) - -logger = logging.getLogger("Slither") - - -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Auto-generate NatSpec documentation for every function using OpenAI Codex.", - usage="slither-documentation filename", - ) - - parser.add_argument("project", help="The target directory/Solidity file.") - - parser.add_argument( - "--overwrite", help="Overwrite the files (be careful).", action="store_true", default=False - ) - parser.add_argument( - "--force-answer-parsing", - help="Apply heuristics to better parse codex output (might lead to incorrect results)", - action="store_true", - default=False, - ) +from slither.tools.codex.utils import log_codex, openai_module - parser.add_argument( - "--include-tests", - help="Include the tests", - action="store_true", - default=False, - ) - parser.add_argument( - "--retry", - help="Retry failed query (default 1). Each retry increases the temperature by 0.1", - action="store", - default=1, - ) - - # Add default arguments from crytic-compile - cryticparser.init(parser) - - codex.init_parser(parser, always_enable_codex=True) - - return parser.parse_args() +logger = logging.getLogger("Codex") def _use_tab(char: str) -> Optional[bool]: @@ -137,7 +89,6 @@ def _handle_codex( return None -# pylint: disable=too-many-locals,too-many-arguments def _handle_function( function: Function, overwrite: bool, @@ -167,12 +118,12 @@ def _handle_function( logger.info("Disable overwrite to avoid mistakes") overwrite = False - openai = codex.openai_module() # type: ignore + openai = openai_module() # type: ignore if openai is None: raise ImportError if logging_file: - codex.log_codex(logging_file, "Q: " + prompt) + log_codex(logging_file, "Q: " + prompt) tentative = 0 answer_processed: Optional[str] = None @@ -187,7 +138,7 @@ def _handle_function( ) if logging_file: - codex.log_codex(logging_file, "A: " + str(answer)) + log_codex(logging_file, "A: " + str(answer)) answer_processed = _handle_codex(answer, src_mapping.starting_column, use_tab, force) if answer_processed: @@ -264,28 +215,3 @@ def _handle_compilation_unit( diff = create_diff(compilation_unit, original_txt, patched_txt, file) with open(f"{file}.patch", "w", encoding="utf8") as f: f.write(diff) - - -def main() -> None: - args = parse_args() - - logger.info("This tool is a WIP, use it with cautious") - logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/") - slither = Slither(args.project, **vars(args)) - - try: - for compilation_unit in slither.compilation_units: - _handle_compilation_unit( - slither, - compilation_unit, - args.overwrite, - args.force_answer_parsing, - int(args.retry), - args.include_tests, - ) - except ImportError: - pass - - -if __name__ == "__main__": - main() diff --git a/slither/tools/codex/utils.py b/slither/tools/codex/utils.py new file mode 100644 index 0000000000..7251f65328 --- /dev/null +++ b/slither/tools/codex/utils.py @@ -0,0 +1,54 @@ +import logging +from pathlib import Path +from typing import Optional + + +logger = logging.getLogger("Slither") + + +# TODO: investigate how to set the correct return type +# So that the other modules can work with openai +def openai_module(api_key: Optional[str] = None): # type: ignore + """ + Return the openai module + Consider checking the usage of open (slither.codex_enabled) before using this function + + Returns: + Optional[the openai module] + """ + try: + # pylint: disable=import-outside-toplevel + import openai + + # Here, we leverage the fact that importing a module in Python is a singleton + # So defining the key the first time is enough for subsequent imports + if api_key is not None: + openai.api_key = api_key + + assert openai.api_key is not None + + except ImportError: + logger.info("OpenAI was not installed") # type: ignore + logger.info('run "pip install openai"') + return None + return openai + + +def log_codex(filename: str, prompt: str) -> None: + """ + Log the prompt in crytic/export/codex/filename + Append to the file + + Args: + filename: filename to write to + prompt: prompt to write + + Returns: + None + """ + + Path("crytic_export/codex").mkdir(parents=True, exist_ok=True) + + with open(Path("crytic_export/codex", filename), "a", encoding="utf8") as file: + file.write(prompt) + file.write("\n") diff --git a/slither/tools/demo/__main__.py b/slither/tools/demo/__main__.py index 5bc2c7c8e2..27ae7b4403 100644 --- a/slither/tools/demo/__main__.py +++ b/slither/tools/demo/__main__.py @@ -1,39 +1,34 @@ -import argparse import logging -from crytic_compile import cryticparser from slither import Slither -logging.basicConfig() -logging.getLogger("Slither").setLevel(logging.INFO) +import typer -logger = logging.getLogger("Slither-demo") +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser(description="Demo", usage="slither-demo filename") +logging.basicConfig() +logging.getLogger("Slither").setLevel(logging.INFO) - parser.add_argument( - "filename", help="The filename of the contract or truffle directory to analyze." - ) +logger = logging.getLogger("Slither-demo") - # Add default arguments from crytic-compile - cryticparser.init(parser) - return parser.parse_args() +demo_app = SlitherApp(help="Demo tool.") +app.add_typer(demo_app, name="demo") -def main() -> None: - args = parse_args() +@demo_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, +) -> None: + state = ctx.ensure_object(SlitherState) # Perform slither analysis on the given filename - _slither = Slither(args.filename, **vars(args)) + _slither = Slither(target.target, **state) logger.info("Analysis done!") if __name__ == "__main__": - main() + demo_app() diff --git a/slither/tools/doctor/__main__.py b/slither/tools/doctor/__main__.py index f401781a77..2d77d8b5b1 100644 --- a/slither/tools/doctor/__main__.py +++ b/slither/tools/doctor/__main__.py @@ -1,42 +1,33 @@ -import argparse import logging import sys -from crytic_compile import cryticparser +import typer from slither.tools.doctor.utils import report_section from slither.tools.doctor.checks import ALL_CHECKS +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Troubleshoot running Slither on your project", - usage="slither-doctor project", - ) +doctor: SlitherApp = SlitherApp() +app.add_typer(doctor, name="doctor") - parser.add_argument("project", help="The codebase to be tested.") - # Add default arguments from crytic-compile - cryticparser.init(parser) - - return parser.parse_args() - - -def main() -> None: +@doctor.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + project: target_type, +) -> None: + """Troubleshoot running Slither on your project.""" # log on stdout to keep output in order logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True) - args = parse_args() - kwargs = vars(args) + state = ctx.ensure_object(SlitherState) for check in ALL_CHECKS: with report_section(check.title): - check.function(**kwargs) + check.function(project=project.target, **state) if __name__ == "__main__": - main() + doctor() diff --git a/slither/tools/erc_conformance/__main__.py b/slither/tools/erc_conformance/__main__.py index 1c9224eacb..63292cd9a4 100644 --- a/slither/tools/erc_conformance/__main__.py +++ b/slither/tools/erc_conformance/__main__.py @@ -1,24 +1,33 @@ -import argparse import logging from collections import defaultdict -from typing import Any, Dict, List, Callable +from pathlib import Path +from typing import Any, Dict, List, Callable, Annotated + +import typer -from crytic_compile import cryticparser from slither import Slither from slither.core.declarations import Contract from slither.utils.erc import ERCS -from slither.utils.output import output_to_json +from slither.utils.output import output_to_json, OutputFormat from .erc.erc1155 import check_erc1155 from .erc.erc20 import check_erc20 from .erc.ercs import generic_erc_checks +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic + +conformance: SlitherApp = SlitherApp() +app.add_typer(conformance, name="check-erc") + + logging.basicConfig() logging.getLogger("Slither").setLevel(logging.INFO) logger = logging.getLogger("Slither-conformance") logger.setLevel(logging.INFO) + ch = logging.StreamHandler() ch.setLevel(logging.INFO) formatter = logging.Formatter("%(message)s") @@ -32,82 +41,62 @@ } -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Check the ERC 20 conformance", - usage="slither-check-erc project contractName", - ) - - parser.add_argument("project", help="The codebase to be tested.") - - parser.add_argument( - "contract_name", - help="The name of the contract. Specify the first case contract that follow the standard. Derived contracts will be checked.", - ) - - parser.add_argument( - "--erc", - help=f"ERC to be tested, available {','.join(ERCS.keys())} (default ERC20)", - action="store", - default="erc20", - ) - - parser.add_argument( - "--json", - help='Export the results as a JSON file ("--json -" to export to stdout)', - action="store", - default=False, - ) - - # Add default arguments from crytic-compile - cryticparser.init(parser) - - return parser.parse_args() - - -def _log_error(err: Any, args: argparse.Namespace) -> None: - if args.json: - output_to_json(args.json, str(err), {"upgradeability-check": []}) +def _log_error(err: Any, output_format: OutputFormat, output_file: Path) -> None: + if output_format == OutputFormat.JSON: + output_to_json(output_file.as_posix(), str(err), {"erc-conformance-check": []}) logger.error(err) -def main() -> None: - args = parse_args() +@conformance.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + contract_name: Annotated[ + str, + typer.Argument( + help="The name of the contract. Specify the first case contract that follow the " + "standard. Derived contracts will be checked." + ), + ], + erc_arg: Annotated[ + str, typer.Option("--erc", help=f"ERC to be tested, available {','.join(ERCS.keys())}.") + ] = "erc20", +) -> None: + """Check the conformance with a specified ERC.""" + state = ctx.ensure_object(SlitherState) # Perform slither analysis on the given filename - slither = Slither(args.project, **vars(args)) + slither = Slither(target.target, **state) ret: Dict[str, List] = defaultdict(list) + output_format = state.get("output_format", OutputFormat.TEXT) + output_file = state.get("output_file", Path("-")) - if args.erc.upper() in ERCS: + if erc_arg.upper() in ERCS: - contracts = slither.get_contract_from_name(args.contract_name) + contracts = slither.get_contract_from_name(contract_name) if len(contracts) != 1: - err = f"Contract not found: {args.contract_name}" - _log_error(err, args) + err = f"Contract not found: {contract_name}" + _log_error(err, output_format=output_format, output_file=output_file) return contract = contracts[0] # First elem is the function, second is the event - erc = ERCS[args.erc.upper()] + erc = ERCS[erc_arg.upper()] generic_erc_checks(contract, erc[0], erc[1], ret) - if args.erc.upper() in ADDITIONAL_CHECKS: - ADDITIONAL_CHECKS[args.erc.upper()](contract, ret) + if erc_arg.upper() in ADDITIONAL_CHECKS: + ADDITIONAL_CHECKS[erc_arg.upper()](contract, ret) else: - err = f"Incorrect ERC selected {args.erc}" - _log_error(err, args) + err = f"Incorrect ERC selected {erc_arg}" + _log_error(err, output_format=output_format, output_file=output_file) return - if args.json: - output_to_json(args.json, None, {"upgradeability-check": ret}) + if output_format == OutputFormat.JSON: + output_to_json(output_file.as_posix(), None, {"erc-conformance-check": ret}) if __name__ == "__main__": - main() + conformance() diff --git a/slither/tools/flattening/__main__.py b/slither/tools/flattening/__main__.py index bf9856fe84..072b26a2e9 100644 --- a/slither/tools/flattening/__main__.py +++ b/slither/tools/flattening/__main__.py @@ -1,142 +1,83 @@ -import argparse import logging -import sys - -from crytic_compile import cryticparser -from crytic_compile.utils.zip import ZIP_TYPES_ACCEPTED +from pathlib import Path +from typing import Annotated, Optional +import typer from slither import Slither from slither.tools.flattening.flattening import ( Flattening, Strategy, - STRATEGIES_NAMES, DEFAULT_EXPORT_PATH, ) +from slither.utils.output import OutputFormat +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic + +flattener: SlitherApp = SlitherApp() +app.add_typer(flattener, name="flat") logging.basicConfig() logger = logging.getLogger("Slither") logger.setLevel(logging.INFO) -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Contracts flattening. See https://github.com/crytic/slither/wiki/Contract-Flattening", - usage="slither-flat filename", - ) - - parser.add_argument("filename", help="The filename of the contract or project to analyze.") - - parser.add_argument("--contract", help="Flatten one contract.", default=None) - - parser.add_argument( - "--strategy", - help=f"Flatenning strategy: {STRATEGIES_NAMES} (default: MostDerived).", - default=Strategy.MostDerived.name, # pylint: disable=no-member - ) - - group_export = parser.add_argument_group("Export options") - - group_export.add_argument( - "--dir", - help=f"Export directory (default: {DEFAULT_EXPORT_PATH}).", - default=None, - ) - - group_export.add_argument( - "--json", - help='Export the results as a JSON file ("--json -" to export to stdout)', - action="store", - default=None, - ) - - parser.add_argument( - "--zip", - help="Export all the files to a zip file", - action="store", - default=None, - ) - - parser.add_argument( - "--zip-type", - help=f"Zip compression type. One of {','.join(ZIP_TYPES_ACCEPTED.keys())}. Default lzma", - action="store", - default=None, - ) - - group_patching = parser.add_argument_group("Patching options") - - group_patching.add_argument( - "--convert-external", help="Convert external to public.", action="store_true" - ) - - group_patching.add_argument( - "--convert-private", - help="Convert private variables to internal.", - action="store_true", - ) - - group_patching.add_argument( - "--convert-library-to-internal", - help="Convert external or public functions to internal in library.", - action="store_true", - ) - - group_patching.add_argument( - "--remove-assert", help="Remove call to assert().", action="store_true" - ) - - group_patching.add_argument( - "--pragma-solidity", - help="Set the solidity pragma with a given version.", - action="store", - default=None, - ) - - # Add default arguments from crytic-compile - cryticparser.init(parser) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -def main() -> None: - args = parse_args() - - slither = Slither(args.filename, **vars(args)) +@flattener.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + contract: Annotated[Optional[str], typer.Option(help="Flatten one contract.")] = None, + strategy: Annotated[ + Strategy, + typer.Option(help="Flattening strategy."), + ] = Strategy.MostDerived, + dir_: Annotated[Path, typer.Option(help="Export directory.")] = DEFAULT_EXPORT_PATH, + convert_external: Annotated[bool, typer.Option(help="Convert external to public.")] = False, + convert_private: Annotated[ + bool, typer.Option(help="Convert private variables to internal.") + ] = False, + convert_library_to_internal: Annotated[ + bool, + typer.Option(help="Convert external or public functions to internal in library."), + ] = False, + remove_assert: Annotated[bool, typer.Option(help="Remove call to assert().")] = False, + pragma_solidity: Annotated[ + Optional[str], typer.Option(help="Set the solidity pragma with a given version.") + ] = None, +) -> None: + """Flatten a contract using the specified strategy.""" + state = ctx.ensure_object(SlitherState) + slither = Slither(target.target, **state) for compilation_unit in slither.compilation_units: flat = Flattening( compilation_unit, - external_to_public=args.convert_external, - remove_assert=args.remove_assert, - convert_library_to_internal=args.convert_library_to_internal, - private_to_internal=args.convert_private, - export_path=args.dir, - pragma_solidity=args.pragma_solidity, + external_to_public=convert_external, + remove_assert=remove_assert, + convert_library_to_internal=convert_library_to_internal, + private_to_internal=convert_private, + export_path=dir_.as_posix(), + pragma_solidity=pragma_solidity, ) - try: - strategy = Strategy[args.strategy] - except KeyError: - to_log = f"{args.strategy} is not a valid strategy, use: {STRATEGIES_NAMES} (default MostDerived)" - logger.error(to_log) - return + json = None + zip_ = None + zip_type = None + + if state.get("output_format") == OutputFormat.JSON: + json = state.get("output_file", Path("-")).as_posix() + elif state.get("output_format") == OutputFormat.ZIP: + zip_ = state.get("output_file", Path("-")).as_posix() + zip_type = state.get("zip_type", "lzma") + flat.export( strategy=strategy, - target=args.contract, - json=args.json, - zip=args.zip, - zip_type=args.zip_type, + target=contract, + json=json, + zip=zip_, + zip_type=zip_type, ) if __name__ == "__main__": - main() + flattener() diff --git a/slither/tools/flattening/flattening.py b/slither/tools/flattening/flattening.py index 9cb2abc3f1..1995d298fb 100644 --- a/slither/tools/flattening/flattening.py +++ b/slither/tools/flattening/flattening.py @@ -35,14 +35,12 @@ Patch = namedtuple("PatchExternal", ["index", "patch_type"]) -class Strategy(PythonEnum): - MostDerived = 0 - OneFile = 1 - LocalImport = 2 +class Strategy(str, PythonEnum): + MostDerived = "most-derived" + OneFile = "one-file" + LocalImport = "local-import" -STRATEGIES_NAMES = ",".join([i.name for i in Strategy]) - DEFAULT_EXPORT_PATH = Path("crytic-export/flattening") diff --git a/slither/tools/interface/__main__.py b/slither/tools/interface/__main__.py index 0705f0373a..4d3731cbae 100644 --- a/slither/tools/interface/__main__.py +++ b/slither/tools/interface/__main__.py @@ -1,88 +1,64 @@ -import argparse import logging from pathlib import Path - -from crytic_compile import cryticparser +from typing import Annotated from slither import Slither from slither.utils.code_generation import generate_interface -logging.basicConfig() -logger = logging.getLogger("Slither-Interface") -logger.setLevel(logging.INFO) +import typer +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Generates code for a Solidity interface from contract", - usage=("slither-interface "), - ) +interface_cmd: SlitherApp = SlitherApp() +app.add_typer(interface_cmd, name="interface") - parser.add_argument( - "contract_source", - help="The name of the contract (case sensitive) followed by the deployed contract address if verified on etherscan or project directory/filename for local contracts.", - nargs="+", - ) - - parser.add_argument( - "--unroll-structs", - help="Whether to use structures' underlying types instead of the user-defined type", - default=False, - action="store_true", - ) - - parser.add_argument( - "--exclude-events", - help="Excludes event signatures in the interface", - default=False, - action="store_true", - ) - - parser.add_argument( - "--exclude-errors", - help="Excludes custom error signatures in the interface", - default=False, - action="store_true", - ) - - parser.add_argument( - "--exclude-enums", - help="Excludes enum definitions in the interface", - default=False, - action="store_true", - ) - - parser.add_argument( - "--exclude-structs", - help="Exclude struct definitions in the interface", - default=False, - action="store_true", - ) - - cryticparser.init(parser) - - return parser.parse_args() +logging.basicConfig() +logger = logging.getLogger("Slither-Interface") +logger.setLevel(logging.INFO) -def main() -> None: - args = parse_args() - contract_name, target = args.contract_source - slither = Slither(target, **vars(args)) +@interface_cmd.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + contract_name: Annotated[ + str, typer.Argument(help="The name of the contract (case sensitive).") + ], + target: target_type, + unroll_structs: Annotated[ + bool, + typer.Option( + help="Whether to use structures' underlying types instead of the user-defined type." + ), + ] = False, + exclude_events: Annotated[ + bool, typer.Option(help="Excludes event signatures in the interface.") + ] = False, + exclude_errors: Annotated[ + bool, typer.Option(help="Excludes custom errors signatures in the interface.") + ] = False, + exclude_enums: Annotated[ + bool, typer.Option(help="Excludes enum definitions in the interface.") + ] = False, + exclude_structs: Annotated[ + bool, typer.Option(help="Excludes structs definitions in the interface.") + ] = False, +) -> None: + """Generates code for a Solidity interface from contract""" + + state = ctx.ensure_object(SlitherState) + slither = Slither(target.target, **state) _contract = slither.get_contract_from_name(contract_name)[0] interface = generate_interface( contract=_contract, - unroll_structs=args.unroll_structs, - include_events=not args.exclude_events, - include_errors=not args.exclude_errors, - include_enums=not args.exclude_enums, - include_structs=not args.exclude_structs, + unroll_structs=unroll_structs, + include_events=not exclude_events, + include_errors=not exclude_errors, + include_enums=not exclude_enums, + include_structs=not exclude_structs, ) # add version pragma @@ -103,4 +79,4 @@ def main() -> None: if __name__ == "__main__": - main() + interface_cmd() diff --git a/slither/tools/kspec_coverage/__main__.py b/slither/tools/kspec_coverage/__main__.py index 19933e0feb..f864964e2d 100644 --- a/slither/tools/kspec_coverage/__main__.py +++ b/slither/tools/kspec_coverage/__main__.py @@ -1,9 +1,17 @@ -import sys import logging -import argparse -from crytic_compile import cryticparser +from typing import Annotated + from slither.tools.kspec_coverage.kspec_coverage import kspec_coverage +import typer + +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherApp, GroupWithCrytic, SlitherState +from slither.utils.output import OutputFormat + +kspec_coverage_app: SlitherApp = SlitherApp() +app.add_typer(kspec_coverage_app, name="check-kspec") + logging.basicConfig() logger = logging.getLogger("Slither.kspec") logger.setLevel(logging.INFO) @@ -16,57 +24,28 @@ logger.propagate = False -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="slither-kspec-coverage", - usage="slither-kspec-coverage contract.sol kspec.md", - ) - - parser.add_argument( - "contract", help="The filename of the contract or truffle directory to analyze." - ) - parser.add_argument( - "kspec", - help="The filename of the Klab spec markdown for the analyzed contract(s)", - ) - - parser.add_argument( - "--version", - help="displays the current version", - version="0.1.0", - action="version", +@kspec_coverage_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + kspec: Annotated[ + str, + typer.Argument(help="The filename of the Klab spec markdown for the analyzed contract(s)"), + ], +) -> None: + """Kspec coverage.""" + state = ctx.ensure_object(SlitherState) + output_json = False + if state.get("output_format") == OutputFormat.JSON: + output_json = state.get("output_file") + + kspec_coverage( + target, + kspec, + ctx, + output_json, ) - parser.add_argument( - "--json", - help='Export the results as a JSON file ("--json -" to export to stdout)', - action="store", - default=False, - ) - - cryticparser.init(parser) - - if len(sys.argv) < 2: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -def main() -> None: - # ------------------------------ - # Usage: slither-kspec-coverage contract kspec - # Example: slither-kspec-coverage contract.sol kspec.md - # ------------------------------ - # Parse all arguments - - args = parse_args() - - kspec_coverage(args) if __name__ == "__main__": - main() + kspec_coverage_app() diff --git a/slither/tools/kspec_coverage/analysis.py b/slither/tools/kspec_coverage/analysis.py index a2ae236606..058165c36a 100755 --- a/slither/tools/kspec_coverage/analysis.py +++ b/slither/tools/kspec_coverage/analysis.py @@ -1,6 +1,6 @@ import logging import re -from argparse import Namespace +from pathlib import Path from typing import Set, Tuple, List, Dict, Union, Optional, Callable from slither.core.compilation_unit import SlitherCompilationUnit @@ -119,7 +119,9 @@ def _generate_output_unresolved( def _run_coverage_analysis( - args: Namespace, slither: SlitherCompilationUnit, kspec_functions: Set[Tuple[str, str]] + slither: SlitherCompilationUnit, + kspec_functions: Set[Tuple[str, str]], + output_json: Union[bool, Path] = False, ) -> None: # Collect all slither functions slither_functions = _get_slither_functions(slither) @@ -141,27 +143,27 @@ def _run_coverage_analysis( kspec_missing.append(slither_func) logger.info("## Check for functions coverage") - json_kspec_present = _generate_output(kspec_present, "[✓]", green, args.json) + json_kspec_present = _generate_output(kspec_present, "[✓]", green, output_json is not False) json_kspec_missing_functions = _generate_output( [f for f in kspec_missing if isinstance(f, FunctionContract)], "[ ] (Missing function)", red, - args.json, + output_json is not False, ) json_kspec_missing_variables = _generate_output( [f for f in kspec_missing if isinstance(f, StateVariable)], "[ ] (Missing variable)", yellow, - args.json, + output_json is not False, ) json_kspec_unresolved = _generate_output_unresolved( - kspec_functions_unresolved, "[ ] (Unresolved)", yellow, args.json + kspec_functions_unresolved, "[ ] (Unresolved)", yellow, output_json is not False ) # Handle unresolved kspecs - if args.json: + if output_json is not False: output.output_to_json( - args.json, + output_json.as_posix(), None, { "functions_present": json_kspec_present, @@ -172,7 +174,9 @@ def _run_coverage_analysis( ) -def run_analysis(args: Namespace, slither: SlitherCompilationUnit, kspec_arg: str) -> None: +def run_analysis( + slither: SlitherCompilationUnit, kspec_arg: str, output_json: Union[bool, Path] = False +) -> None: # Get all of our kspec'd functions (tuple(contract_name, function_name)). if "," in kspec_arg: kspecs = kspec_arg.split(",") @@ -183,4 +187,4 @@ def run_analysis(args: Namespace, slither: SlitherCompilationUnit, kspec_arg: st kspec_functions = _get_all_covered_kspec_functions(kspec_arg) # Run coverage analysis - _run_coverage_analysis(args, slither, kspec_functions) + _run_coverage_analysis(slither, kspec_functions, output_json) diff --git a/slither/tools/kspec_coverage/kspec_coverage.py b/slither/tools/kspec_coverage/kspec_coverage.py index f8c2d8cf2e..c8c64ed157 100755 --- a/slither/tools/kspec_coverage/kspec_coverage.py +++ b/slither/tools/kspec_coverage/kspec_coverage.py @@ -1,19 +1,23 @@ -import argparse +from pathlib import Path +from typing import Union + +import typer from slither.tools.kspec_coverage.analysis import run_analysis from slither import Slither +from slither.utils.command_line import SlitherState -def kspec_coverage(args: argparse.Namespace) -> None: - - contract = args.contract - kspec = args.kspec +def kspec_coverage( + contract: str, kspec: str, ctx: typer.Context, output_json: Union[bool, Path] = False +) -> None: - slither = Slither(contract, **vars(args)) + state = ctx.ensure_object(SlitherState) + slither = Slither(contract, **state) compilation_units = slither.compilation_units if len(compilation_units) != 1: print("Only single compilation unit supported") return # Run the analysis on the Klab specs - run_analysis(args, compilation_units[0], kspec) + run_analysis(compilation_units[0], kspec, output_json) diff --git a/slither/tools/mutator/__main__.py b/slither/tools/mutator/__main__.py index 8a7ce3e1ab..647584176b 100644 --- a/slither/tools/mutator/__main__.py +++ b/slither/tools/mutator/__main__.py @@ -1,13 +1,12 @@ -import argparse import inspect import logging import os import shutil -import sys import time from pathlib import Path -from typing import Type, List, Any, Optional -from crytic_compile import cryticparser +import typer +from typing import Type, List, Optional, Annotated, Union + from slither import Slither from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd from slither.tools.mutator.mutators import all_mutators @@ -19,11 +18,23 @@ backup_source_file, get_sol_file_list, ) +from slither.__main__ import app +from slither.utils.command_line import ( + target_type, + SlitherState, + SlitherApp, + GroupWithCrytic, + CommaSeparatedValueParser, +) + +mutate_cmd: SlitherApp = SlitherApp() +app.add_typer(mutate_cmd, name="mutate") logging.basicConfig() logger = logging.getLogger("Slither-Mutate") logger.setLevel(logging.INFO) + ################################################################################### ################################################################################### # region Cli Arguments @@ -31,92 +42,7 @@ ################################################################################### -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 --test-cmd ", - ) - - parser.add_argument("codebase", help="Codebase to analyze (.sol file, project directory, ...)") - - parser.add_argument( - "--list-mutators", - help="List available detectors", - action=ListMutators, - nargs=0, - 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( - "-v", - "--verbose", - help="log mutants that are caught as well as those that are uncaught", - action="store_true", - default=False, - ) - - # to print just all the mutants - parser.add_argument( - "-vv", - "--very-verbose", - help="log mutants that are caught, uncaught, and fail to compile. And more!", - 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( - "--comprehensive", - help="continue testing minor mutations if severe mutants are uncaught", - action="store_true", - default=False, - ) - - # Initiate all the crytic config cli options - cryticparser.init(parser) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]: +def _get_mutators(mutators_list: Union[List[str], None]) -> List[Type[AbstractMutator]]: detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)] if mutators_list is not None: detectors = [ @@ -131,13 +57,14 @@ def _get_mutators(mutators_list: List[str] | None) -> List[Type[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(None) - output_mutators(checks) - parser.exit() +def list_mutator_action(ctx: typer.Context, value: bool) -> None: + """List mutators.""" + if not value or ctx.resilient_parsing: + return + + checks = _get_mutators(None) + output_mutators(checks) + raise typer.Exit() # endregion @@ -148,22 +75,58 @@ def __call__( ################################################################################### -def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals - args = parse_args() +@mutate_cmd.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + codebase: Annotated[Path, typer.Argument(help="Codebase directory.")], + test_command: Annotated[ + str, typer.Option("--test-cmd", help="Command to run the tests for your project.") + ], + test_directory: Annotated[ + Optional[str], typer.Option("--test-dir", help="Tests directory.") + ] = None, + output_dir: Annotated[ + Optional[Path], typer.Option("--output-dir", help="Output directory.") + ] = Path("mutation_campaign"), + paths_to_ignore: Annotated[ + Optional[str], typer.Option("--ignore-dirs", help="Directories to ignore.") + ] = None, + timeout: Annotated[int, typer.Option("--timeout", help="Test timeout.")] = 30, + list_mutators: Annotated[ + bool, + typer.Option( + "--list-mutators", is_eager=True, help="List mutators.", callback=list_mutator_action + ), + ] = False, + mutators_to_run: Annotated[ + Optional[str], typer.Option(help="Mutant generators to run.") + ] = None, + verbose_count: Annotated[int, typer.Option("--verbose", "-v", count=True, max=2)] = 0, + contract_names: Annotated[ + List[str], + typer.Option( + help="List of contract names you want to mutate", click_type=CommaSeparatedValueParser() + ), + ] = None, + comprehensive_flag: Annotated[ + bool, typer.Option(help="Continue testing minor mutations if severe mutants are uncaught.") + ] = False, +) -> None: # pylint: disable=too-many-statements,too-many-branches,too-many-locals + """Experimental smart contract mutator. Based on https://arxiv.org/abs/2006.11597.""" # 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 - very_verbose: Optional[bool] = args.very_verbose - mutators_to_run: Optional[List[str]] = args.mutators_to_run - comprehensive_flag: Optional[bool] = args.comprehensive - - logger.info(blue(f"Starting mutation campaign in {args.codebase}")) + # test_command: str = args.test_cmd + state = ctx.ensure_object(SlitherState) + solc_remappings: Optional[str] = state.get("solc_remaps") + + verbose = False + very_verbose = False + if verbose_count >= 1: + verbose = True + if verbose_count >= 2: + very_verbose = True + + logger.info(blue(f"Starting mutation campaign in {codebase}")) if paths_to_ignore: paths_to_ignore_list = paths_to_ignore.strip("][").split(",") @@ -171,18 +134,11 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too else: paths_to_ignore_list = [] - contract_names: List[str] = [] - if args.contract_names: - contract_names = args.contract_names.split(",") - # get all the contracts as a list from given codebase - sol_file_list: List[str] = get_sol_file_list(Path(args.codebase), paths_to_ignore_list) + sol_file_list: List[str] = get_sol_file_list(codebase, paths_to_ignore_list) # folder where backup files and uncaught mutants are saved - if output_dir is None: - output_dir = "./mutation_campaign" - - output_folder = Path(output_dir).resolve() + output_folder = output_dir.resolve() if output_folder.is_dir(): shutil.rmtree(output_folder) @@ -237,7 +193,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too for filename in sol_file_list: # pylint: disable=too-many-nested-blocks file_name = os.path.split(filename)[1].split(".sol")[0] # slither object - sl = Slither(filename, **vars(args)) + sl = Slither(filename, **state) # create a backup files files_dict = backup_source_file(sl.source_code, output_folder) # total revert/comment/tweak mutants that were generated and compiled @@ -347,7 +303,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too if total_mutant_counts[0] > 0: logger.info( magenta( - f"Revert mutants: {uncaught_mutant_counts[0]} uncaught of {total_mutant_counts[0]} ({100 * uncaught_mutant_counts[0]/total_mutant_counts[0]}%)" + f"Revert mutants: {uncaught_mutant_counts[0]} uncaught of {total_mutant_counts[0]} ({100 * uncaught_mutant_counts[0] / total_mutant_counts[0]}%)" ) ) else: @@ -356,7 +312,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too if total_mutant_counts[1] > 0: logger.info( magenta( - f"Comment mutants: {uncaught_mutant_counts[1]} uncaught of {total_mutant_counts[1]} ({100 * uncaught_mutant_counts[1]/total_mutant_counts[1]}%)" + f"Comment mutants: {uncaught_mutant_counts[1]} uncaught of {total_mutant_counts[1]} ({100 * uncaught_mutant_counts[1] / total_mutant_counts[1]}%)" ) ) else: @@ -365,7 +321,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too if total_mutant_counts[2] > 0: logger.info( magenta( - f"Tweak mutants: {uncaught_mutant_counts[2]} uncaught of {total_mutant_counts[2]} ({100 * uncaught_mutant_counts[2]/total_mutant_counts[2]}%)\n" + f"Tweak mutants: {uncaught_mutant_counts[2]} uncaught of {total_mutant_counts[2]} ({100 * uncaught_mutant_counts[2] / total_mutant_counts[2]}%)\n" ) ) else: @@ -392,9 +348,7 @@ def main() -> None: # pylint: disable=too-many-statements,too-many-branches,too else: elapsed_string = f"{seconds} {'second' if seconds == 1 else 'seconds'}" - logger.info( - blue(f"Finished mutation testing assessment of '{args.codebase}' in {elapsed_string}\n") - ) + logger.info(blue(f"Finished mutation testing assessment of '{codebase}' in {elapsed_string}\n")) # endregion diff --git a/slither/tools/possible_paths/__main__.py b/slither/tools/possible_paths/__main__.py index b993d266a1..4cc48495ad 100644 --- a/slither/tools/possible_paths/__main__.py +++ b/slither/tools/possible_paths/__main__.py @@ -1,9 +1,8 @@ -import sys - import logging -from argparse import ArgumentParser, Namespace +from typing import Annotated, List + +import typer -from crytic_compile import cryticparser from slither import Slither from slither.core.declarations import FunctionContract from slither.utils.colors import red @@ -13,48 +12,37 @@ ResolveFunctionException, ) -logging.basicConfig() -logging.getLogger("Slither").setLevel(logging.INFO) - +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic -def parse_args() -> Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser: ArgumentParser = ArgumentParser( - description="PossiblePaths", - usage="possible_paths.py filename [contract.function targets]", - ) +possible_paths_app: SlitherApp = SlitherApp() +app.add_typer(possible_paths_app, name="find-paths") - parser.add_argument( - "filename", help="The filename of the contract or truffle directory to analyze." - ) - - parser.add_argument("targets", nargs="+") - - cryticparser.init(parser) - - return parser.parse_args() +logging.basicConfig() +logging.getLogger("Slither").setLevel(logging.INFO) -def main() -> None: - # ------------------------------ - # PossiblePaths.py - # Usage: python3 possible_paths.py filename targets - # Example: python3 possible_paths.py contract.sol contract1.function1 contract2.function2 contract3.function3 - # ------------------------------ - # Parse all arguments - args = parse_args() - # Perform slither analysis on the given filename - slither = Slither(args.filename, **vars(args)) +@possible_paths_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + functions: Annotated[ + List[str], + typer.Argument( + help="Function to analyze. Should be noted as contract.function. Can be repeated." + ), + ], +) -> None: + """Find the possible paths.""" + state = ctx.ensure_object(SlitherState) + slither = Slither(target.target, **state) try: - targets = resolve_functions(slither, args.targets) - except ResolveFunctionException as resolvefunction: - print(red(resolvefunction)) - sys.exit(-1) + targets = resolve_functions(slither, functions) + except ResolveFunctionException as resolve_function: + print(red(resolve_function)) + raise typer.Exit(1) # Print out all target functions. print("Target functions:") @@ -89,4 +77,4 @@ def main() -> None: if __name__ == "__main__": - main() + possible_paths_app() diff --git a/slither/tools/properties/__main__.py b/slither/tools/properties/__main__.py index b5e5c911a3..54c9720a26 100644 --- a/slither/tools/properties/__main__.py +++ b/slither/tools/properties/__main__.py @@ -1,9 +1,6 @@ -import argparse import logging -import sys -from typing import Any - -from crytic_compile import cryticparser +from typing import Annotated +import typer from slither import Slither from slither.tools.properties.properties.erc20 import generate_erc20, ERC20_PROPERTIES @@ -15,6 +12,14 @@ ) from slither.utils.myprettytable import MyPrettyTable +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic + + +properties_app: SlitherApp = SlitherApp() +app.add_typer(properties_app, name="prop") + + logging.basicConfig() logging.getLogger("Slither").setLevel(logging.INFO) @@ -46,109 +51,86 @@ def _all_properties() -> MyPrettyTable: return table -class ListScenarios(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - logger.info(_all_scenarios()) - parser.exit() - - -class ListProperties(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - logger.info(_all_properties()) - parser.exit() - - -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Generates code properties (e.g., invariants) that can be tested with unit tests or Echidna, entirely automatically.", - usage="slither-prop filename", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - parser.add_argument( - "filename", help="The filename of the contract or project directory to analyze." - ) - - parser.add_argument("--contract", help="The targeted contract.") - - parser.add_argument( - "--scenario", - help="Test a specific scenario. Use --list-scenarios to see the available scenarios. Default Transferable", - default="Transferable", - ) - - parser.add_argument( - "--list-scenarios", - help="List available scenarios", - action=ListScenarios, - nargs=0, - default=False, - ) - - parser.add_argument( - "--list-properties", - help="List available properties", - action=ListProperties, - nargs=0, - default=False, - ) - - parser.add_argument( - "--address-owner", help=f"Owner address. Default {OWNER_ADDRESS}", default=None - ) - - parser.add_argument( - "--address-user", help=f"Owner address. Default {USER_ADDRESS}", default=None - ) - - parser.add_argument( - "--address-attacker", - help=f"Attacker address. Default {ATTACKER_ADDRESS}", - default=None, - ) - - # Add default arguments from crytic-compile - cryticparser.init(parser) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -def main() -> None: - args = parse_args() +def list_scenarios_action(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + logger.info(_all_scenarios()) + raise typer.Exit() + + +def list_properties_action(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + logger.info(_all_properties()) + raise typer.Exit() + + +@properties_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + contract: Annotated[str, typer.Option(help="The targeted contract.")], + scenario: Annotated[ + str, + typer.Option( + help="Test a specific scenario. Use --list-scenarios to see the available scenarios." + ), + ] = "Transferable", + list_scenarios: Annotated[ + bool, + typer.Option( + "--list-scenarios", + help="List available scenarios", + callback=list_scenarios_action, + is_eager=True, + ), + ] = False, + list_properties: Annotated[ + bool, + typer.Option( + "--list-properties", + help="List available properties", + callback=list_properties_action, + is_eager=True, + ), + ] = False, + address_owner: Annotated[ + str, typer.Option("--address-owner", help="Owner address.") + ] = OWNER_ADDRESS, + address_user: Annotated[ + str, typer.Option("--address-user", help="User address.") + ] = USER_ADDRESS, + address_attacker: Annotated[ + str, typer.Option("--address-attacker", help="Attacker address.") + ] = ATTACKER_ADDRESS, +) -> None: + """Generates code properties (e.g., invariants) that can be tested with unit tests or Echidna, + entirely automatically.""" # Perform slither analysis on the given filename - slither = Slither(args.filename, **vars(args)) + state = ctx.ensure_object(SlitherState) + slither = Slither(target.target, **state) - contracts = slither.get_contract_from_name(args.contract) + contracts = slither.get_contract_from_name(contract) if len(contracts) != 1: if len(slither.contracts) == 1: contract = slither.contracts[0] else: - if args.contract is None: + if contract is None: to_log = "Specify the target: --contract ContractName" else: - to_log = f"{args.contract} not found" + to_log = f"{contract} not found" logger.error(to_log) - return + raise typer.Exit(1) else: contract = contracts[0] - addresses = Addresses(args.address_owner, args.address_user, args.address_attacker) + addresses = Addresses(address_owner, address_user, address_attacker) - generate_erc20(contract, args.scenario, addresses) + generate_erc20(contract, scenario, addresses) if __name__ == "__main__": - main() + properties_app() diff --git a/slither/tools/read_storage/__main__.py b/slither/tools/read_storage/__main__.py index 3baa5d351a..f35f7d6c61 100644 --- a/slither/tools/read_storage/__main__.py +++ b/slither/tools/read_storage/__main__.py @@ -2,177 +2,168 @@ Tool to read on-chain storage from EVM """ import json -import argparse +from pathlib import Path +from typing import Annotated, List, Optional -from crytic_compile import cryticparser +import typer from slither import Slither from slither.exceptions import SlitherError -from slither.tools.read_storage.read_storage import SlitherReadStorage, RpcInfo +import slither.tools.read_storage as rs +from slither.__main__ import app +from slither.utils.command_line import SlitherState, SlitherApp, GroupWithCrytic -def parse_args() -> argparse.Namespace: - """Parse the underlying arguments for the program. - Returns: - The arguments for the program. - """ - parser = argparse.ArgumentParser( - description="Read a variable's value from storage for a deployed contract", - usage=( - "\nTo retrieve a single variable's value:\n" - + "\tslither-read-storage $TARGET address --variable-name $NAME\n" - + "To retrieve a contract's storage layout:\n" - + "\tslither-read-storage $TARGET address --contract-name $NAME --json storage_layout.json\n" - + "To retrieve a contract's storage layout and values:\n" - + "\tslither-read-storage $TARGET address --contract-name $NAME --json storage_layout.json --value\n" - + "TARGET can be a contract address or project directory" +read_storage: SlitherApp = SlitherApp() +app.add_typer(read_storage, name="read-storage") + + +@read_storage.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + contract_source: Annotated[ + List[str], + typer.Argument( + ..., + help="The deployed contract address if verified on etherscan or prepend project " + "directory for unverified contracts.", + ), + ], + variable_name: Annotated[ + Optional[str], + typer.Option( + "--variable-name", help="The name of the variable whose value will be returned." + ), + ] = None, + rpc_url: Annotated[ + Optional[str], typer.Option("--rpc-url", help="An endpoint for web3 requests.") + ] = None, + key: Annotated[ + Optional[str], + typer.Option( + "--key", help="The key/index whose value will be returned from a mapping or array." + ), + ] = None, + deep_key: Annotated[ + Optional[str], + typer.Option( + "--deep-key", + help="The key/index whose value will be returned from a deep mapping or multidimensional array.", + ), + ] = None, + struct_var: Annotated[ + Optional[str], + typer.Option( + "--struct-var", + help="The name of the variable whose value will be returned from a struct.", ), - ) - - parser.add_argument( - "contract_source", - help="The deployed contract address if verified on etherscan. Prepend project directory for unverified contracts.", - nargs="+", - ) - - parser.add_argument( - "--variable-name", - help="The name of the variable whose value will be returned.", - default=None, - ) - - parser.add_argument("--rpc-url", help="An endpoint for web3 requests.") - - parser.add_argument( - "--key", - help="The key/ index whose value will be returned from a mapping or array.", - default=None, - ) - - parser.add_argument( - "--deep-key", - help="The key/ index whose value will be returned from a deep mapping or multidimensional array.", - default=None, - ) - - parser.add_argument( - "--struct-var", - help="The name of the variable whose value will be returned from a struct.", - default=None, - ) - - parser.add_argument( - "--storage-address", - help="The address of the storage contract (if a proxy pattern is used).", - default=None, - ) - - parser.add_argument( - "--contract-name", - help="The name of the logic contract.", - default=None, - ) - - parser.add_argument( - "--json", - action="store", - help="Save the result in a JSON file.", - ) - - parser.add_argument( - "--value", - action="store_true", - help="Toggle used to include values in output.", - ) - - parser.add_argument( - "--table", - action="store_true", - help="Print table view of storage layout", - ) - - parser.add_argument( - "--silent", - action="store_true", - help="Silence log outputs", - ) - - parser.add_argument("--max-depth", help="Max depth to search in data structure.", default=20) - - parser.add_argument( - "--block", - help="The block number to read storage from. Requires an archive node to be provided as the RPC url.", - default="latest", - ) - - parser.add_argument( - "--unstructured", - action="store_true", - help="Include unstructured storage slots", - ) - - cryticparser.init(parser) - - return parser.parse_args() - - -def main() -> None: - args = parse_args() - - if len(args.contract_source) == 2: + ] = None, + storage_address: Annotated[ + Optional[str], + typer.Option( + "--storage-address", + help="The address of the storage contract if a proxy pattern is used.", + ), + ] = None, + contract_name: Annotated[ + Optional[str], typer.Option("--contract-name", help="The name of the logic contract.") + ] = None, + value: Annotated[ + bool, typer.Option("--value", help="Toggle used to include values in output.", is_flag=True) + ] = False, + table: Annotated[ + bool, typer.Option("--table", help="Print table view of storage layout.", is_flag=True) + ] = False, + silent: Annotated[ + bool, typer.Option("--silent", help="Silence log outputs.", is_flag=True) + ] = False, + max_depth: Annotated[ + int, typer.Option("--max-depth", help="Max depth to search in data structure.") + ] = 20, + block: Annotated[ + str, + typer.Option( + "--block", + help="The block number to read storage from. Requires an archive node to be provided as " + "the RPC url.", + ), + ] = "latest", + unstructured: Annotated[ + bool, + typer.Option("--unstructured", help="Include unstructured storage slots.", is_flag=True), + ] = False, +) -> None: + """Read a variable's value from storage for a deployed contract. + + To retrieve a single variable's value: + slither read-storage $TARGET address --variable-name $NAME + To retrieve a contract's storage layout: + slither read-storage $TARGET address --contract-name $NAME --json storage_layout.json + To retrieve a contract's storage layout and values: + slither read-storage $TARGET address --contract-name $NAME --json storage_layout.json --value + """ + + state = ctx.ensure_object(SlitherState) + + if len(contract_source) == 2: # Source code is file.sol or project directory - source_code, target = args.contract_source - slither = Slither(source_code, **vars(args)) + source_code, target = contract_source + slither = Slither(source_code, **state) else: # Source code is published and retrieved via etherscan - target = args.contract_source[0] - slither = Slither(target, **vars(args)) + target = contract_source[0] + slither = Slither(target, **state) - if args.contract_name: - contracts = slither.get_contract_from_name(args.contract_name) + if contract_name: + contracts = slither.get_contract_from_name(contract_name) if len(contracts) == 0: - raise SlitherError(f"Contract {args.contract_name} not found.") + raise SlitherError(f"Contract {contract_name} not found.") else: contracts = slither.contracts rpc_info = None - if args.rpc_url: + if rpc_url: valid = ["latest", "earliest", "pending", "safe", "finalized"] - block = args.block if args.block in valid else int(args.block) - rpc_info = RpcInfo(args.rpc_url, block) + block = block if block in valid else int(block) + rpc_info = rs.RpcInfo(rpc_url, block) - srs = SlitherReadStorage(contracts, args.max_depth, rpc_info) - srs.unstructured = bool(args.unstructured) + srs = rs.SlitherReadStorage(contracts, max_depth, rpc_info) + srs.unstructured = unstructured # Remove target prefix e.g. rinkeby:0x0 -> 0x0. address = target[target.find(":") + 1 :] # Default to implementation address unless a storage address is given. - if not args.storage_address: - args.storage_address = address - srs.storage_address = args.storage_address + if not storage_address: + storage_address = address + srs.storage_address = storage_address - if args.variable_name: + if variable_name: # Use a lambda func to only return variables that have same name as target. # x is a tuple (`Contract`, `StateVariable`). - srs.get_all_storage_variables(lambda x: bool(x[1].name == args.variable_name)) - srs.get_target_variables(**vars(args)) + srs.get_all_storage_variables(lambda x: bool(x[1].name == variable_name)) + # FIXME: Dirty way of passing all needed values. + srs.get_target_variables(**locals()) else: srs.get_all_storage_variables() srs.get_storage_layout() # To retrieve slot values an rpc url is required. - if args.value: - assert args.rpc_url + if value: + assert rpc_url srs.walk_slot_info(srs.get_slot_values) - if args.table: + if table: srs.walk_slot_info(srs.convert_slot_info_to_rows) print(srs.table) - if args.json: - with open(args.json, "w", encoding="utf-8") as file: - slot_infos_json = srs.to_json() - json.dump(slot_infos_json, file, indent=4) + from slither.utils.output import OutputFormat + + if state.get("output_format") == OutputFormat.JSON: + output_file = state.get("output_file") + if output_file != Path("-"): + with open(output_file, "w", encoding="utf-8") as file: + slot_infos_json = srs.to_json() + json.dump(slot_infos_json, file, indent=4) if __name__ == "__main__": - main() + read_storage() diff --git a/slither/tools/read_storage/utils/utils.py b/slither/tools/read_storage/utils/utils.py index 20e7c1372f..85c3ccdfbb 100644 --- a/slither/tools/read_storage/utils/utils.py +++ b/slither/tools/read_storage/utils/utils.py @@ -1,6 +1,7 @@ from typing import Union from eth_typing.evm import ChecksumAddress + from eth_utils import to_int, to_text, to_checksum_address from web3 import Web3 diff --git a/slither/tools/similarity/__init__.py b/slither/tools/similarity/__init__.py index b31b92c608..e69de29bb2 100644 --- a/slither/tools/similarity/__init__.py +++ b/slither/tools/similarity/__init__.py @@ -1 +0,0 @@ -from .model import load_model diff --git a/slither/tools/similarity/__main__.py b/slither/tools/similarity/__main__.py index 86673fccd4..7dfba06b3f 100755 --- a/slither/tools/similarity/__main__.py +++ b/slither/tools/similarity/__main__.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 - -import argparse +import enum import logging -import sys +from typing import Annotated, Optional + +import typer -from crytic_compile import cryticparser +from slither.__main__ import app +from slither.utils.command_line import SlitherState, SlitherApp, GroupWithCrytic + +similarity: SlitherApp = SlitherApp() +app.add_typer(similarity, name="simil") -from slither.tools.similarity.info import info -from slither.tools.similarity.test import test -from slither.tools.similarity.train import train -from slither.tools.similarity.plot import plot logging.basicConfig() logger = logging.getLogger("Slither-simil") @@ -17,90 +18,73 @@ modes = ["info", "test", "train", "plot"] -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Code similarity detection tool. For usage, see https://github.com/crytic/slither/wiki/Code-Similarity-detector" - ) - - parser.add_argument("mode", help="|".join(modes)) - - parser.add_argument("model", help="model.bin") - - parser.add_argument("--filename", action="store", dest="filename", help="contract.sol") - - parser.add_argument("--fname", action="store", dest="fname", help="Target function") - - parser.add_argument("--ext", action="store", dest="ext", help="Extension to filter contracts") - - parser.add_argument( - "--nsamples", - action="store", - type=int, - dest="nsamples", - help="Number of contract samples used for training", - ) - - parser.add_argument( - "--ntop", - action="store", - type=int, - dest="ntop", - default=10, - help="Number of more similar contracts to show for testing", - ) +class Mode(str, enum.Enum): + info = "info" + test = "test" + train = "train" + plot = "plot" + + +@similarity.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + mode: Annotated[Mode, typer.Option(help="Operation mode")] = Mode.info, + model: Annotated[str, typer.Argument(help="Model filename")] = "model.bin", + filename: Annotated[ + Optional[str], typer.Option("--filename", help="Contract file name (e.g., contract.sol)") + ] = None, + fname: Annotated[Optional[str], typer.Option("--fname", help="Target function name")] = None, + ext: Annotated[ + Optional[str], typer.Option("--ext", help="Extension to filter contracts by") + ] = None, + nsamples: Annotated[ + int, typer.Option("--nsamples", help="Number of contract samples used for training") + ] = 0, + ntop: Annotated[ + int, typer.Option(help="Number of most similar contracts to show for testing") + ] = 10, + input_: Annotated[ + Optional[str], typer.Option("--input", help="File or directory used as input") + ] = None, +) -> None: + """Code similarity detection tool. + + For usage, see https://github.com/crytic/slither/wiki/Code-Similarity-detector + """ - parser.add_argument( - "--input", action="store", dest="input", help="File or directory used as input" - ) + default_log = logging.INFO + logger.setLevel(default_log) - parser.add_argument( - "--version", - help="displays the current version", - version="0.0", - action="version", + state = ctx.ensure_object(SlitherState) + state.update( + { + "model": model, + "filename": filename, + "fname": fname, + "ext": ext, + "nsamples": nsamples, + "ntop": ntop, + "input_": input_, + } ) - cryticparser.init(parser) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - args = parser.parse_args() - return args - - -# endregion -################################################################################### -################################################################################### -# region Main -################################################################################### -################################################################################### - - -def main() -> None: - args = parse_args() - - default_log = logging.INFO - logger.setLevel(default_log) + from slither.tools.similarity.info import info + from slither.tools.similarity.test import test + from slither.tools.similarity.train import train + from slither.tools.similarity.plot import plot - mode = args.mode + mapping = { + Mode.info: info, + Mode.test: test, + Mode.train: train, + Mode.plot: plot, + } - if mode == "info": - info(args) - elif mode == "train": - train(args) - elif mode == "test": - test(args) - elif mode == "plot": - plot(args) - else: - to_log = f"Invalid mode!. It should be one of these: {', '.join(modes)}" - logger.error(to_log) - sys.exit(-1) + func = mapping[mode] + func(**state) if __name__ == "__main__": - main() + similarity() # endregion diff --git a/slither/tools/similarity/cache.py b/slither/tools/similarity/cache.py index ccd64b84b9..a351a12410 100644 --- a/slither/tools/similarity/cache.py +++ b/slither/tools/similarity/cache.py @@ -6,7 +6,6 @@ except ImportError: print("ERROR: in order to use slither-simil, you need to install numpy") print("$ pip3 install numpy --user\n") - sys.exit(-1) def load_cache(infile: str, nsamples: Optional[int] = None) -> Dict: diff --git a/slither/tools/similarity/info.py b/slither/tools/similarity/info.py index c9f9753d13..76c90f3f55 100644 --- a/slither/tools/similarity/info.py +++ b/slither/tools/similarity/info.py @@ -1,4 +1,3 @@ -import argparse import logging import sys import os.path @@ -11,21 +10,21 @@ logger = logging.getLogger("Slither-simil") -def info(args: argparse.Namespace) -> None: +def info(**kwargs) -> None: try: - model = args.model + model = kwargs.get("model") if os.path.isfile(model): model = load_model(model) else: model = None - filename = args.filename - contract, fname = parse_target(args.fname) + filename = kwargs.get("filename") + contract, fname = parse_target(kwargs.get("fname")) if filename is None and contract is None and fname is None: - logger.info("%s uses the following words:", args.model) + logger.info("%s uses the following words:", kwargs.get("model")) for word in model.get_words(): logger.info(word) sys.exit(0) @@ -34,7 +33,7 @@ def info(args: argparse.Namespace) -> None: logger.error("The encode mode requires filename, contract and fname parameters.") sys.exit(-1) - irs = encode_contract(filename, **vars(args)) + irs = encode_contract(filename, **kwargs) if len(irs) == 0: sys.exit(-1) @@ -49,7 +48,7 @@ def info(args: argparse.Namespace) -> None: logger.info(fvector) except Exception: # pylint: disable=broad-except - to_log = f"Error in {args.filename}" + to_log = f"Error in {kwargs.get('filename')}" logger.error(to_log) logger.error(traceback.format_exc()) sys.exit(-1) diff --git a/slither/tools/similarity/model.py b/slither/tools/similarity/model.py index d035f81cfa..eb9db23fb3 100644 --- a/slither/tools/similarity/model.py +++ b/slither/tools/similarity/model.py @@ -1,5 +1,3 @@ -import sys - try: # pylint: disable=unused-import from fastText import load_model @@ -7,4 +5,3 @@ except ImportError: print("ERROR: in order to use slither-simil, you need to install fastText 0.2.0:") print("$ pip3 install https://github.com/facebookresearch/fastText/archive/0.2.0.zip --user\n") - sys.exit(-1) diff --git a/slither/tools/similarity/plot.py b/slither/tools/similarity/plot.py index f11e921293..75003d4a4c 100644 --- a/slither/tools/similarity/plot.py +++ b/slither/tools/similarity/plot.py @@ -1,4 +1,3 @@ -import argparse import logging import random import sys @@ -9,7 +8,6 @@ except ImportError: print("ERROR: in order to use slither-simil, you need to install numpy:") print("$ pip3 install numpy --user\n") - sys.exit(-1) from slither.tools.similarity.encode import load_and_encode, parse_target from slither.tools.similarity.model import load_model @@ -24,7 +22,7 @@ logger = logging.getLogger("Slither-simil") -def plot(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals +def plot(**kwargs) -> None: # pylint: disable=too-many-locals if decomposition is None or plt is None: logger.error( @@ -35,12 +33,11 @@ def plot(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals try: - model = args.model - model = load_model(model) + model = load_model(kwargs.get("model")) # contract = args.contract - contract, fname = parse_target(args.fname) + contract, fname = parse_target(kwargs.get("fname")) # solc = args.solc - infile = args.input + infile = kwargs.get("input") # ext = args.filter # nsamples = args.nsamples @@ -49,7 +46,7 @@ def plot(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals sys.exit(-1) logger.info("Loading data..") - cache = load_and_encode(infile, **vars(args)) + cache = load_and_encode(infile, **kwargs) data = [] fs = [] @@ -81,6 +78,6 @@ def plot(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals plt.savefig("plot.png", bbox_inches="tight") except Exception: # pylint: disable=broad-except - logger.error(f"Error in {args.filename}") + logger.error(f"Error in {kwargs.get('filename')}") logger.error(traceback.format_exc()) sys.exit(-1) diff --git a/slither/tools/similarity/similarity.py b/slither/tools/similarity/similarity.py index e0e897b5ca..6abb4f4f20 100644 --- a/slither/tools/similarity/similarity.py +++ b/slither/tools/similarity/similarity.py @@ -5,7 +5,6 @@ except ImportError: print("ERROR: in order to use slither-simil, you need to install numpy:") print("$ pip3 install numpy --user\n") - sys.exit(-1) def similarity(v1, v2): diff --git a/slither/tools/similarity/test.py b/slither/tools/similarity/test.py index 7d42c4a63f..2d57167c97 100755 --- a/slither/tools/similarity/test.py +++ b/slither/tools/similarity/test.py @@ -2,7 +2,6 @@ import operator import sys import traceback -from argparse import Namespace from slither.tools.similarity.encode import encode_contract, load_and_encode, parse_target from slither.tools.similarity.model import load_model @@ -11,28 +10,28 @@ logger = logging.getLogger("Slither-simil") -def test(args: Namespace) -> None: +def test(**kwargs) -> None: try: - model = args.model + model = kwargs.get("model") model = load_model(model) - filename = args.filename - contract, fname = parse_target(args.fname) - infile = args.input - ntop = args.ntop + filename = kwargs.get("filename") + contract, fname = parse_target(kwargs.get("fname")) + infile = kwargs.get("input") + ntop = kwargs.get("ntop") if filename is None or contract is None or fname is None or infile is None: logger.error("The test mode requires filename, contract, fname and input parameters.") sys.exit(-1) - irs = encode_contract(filename, **vars(args)) + irs = encode_contract(filename, **kwargs) if len(irs) == 0: sys.exit(-1) y = " ".join(irs[(filename, contract, fname)]) fvector = model.get_sentence_vector(y) - cache = load_and_encode(infile, model, **vars(args)) + cache = load_and_encode(infile, model, **kwargs) # save_cache("cache.npz", cache) r = {} @@ -48,6 +47,6 @@ def test(args: Namespace) -> None: logger.info(format_table.format(*(list(x) + [score]))) except Exception: # pylint: disable=broad-except - logger.error(f"Error in {args.filename}") + logger.error(f"Error in {kwargs.get('filename')}") logger.error(traceback.format_exc()) sys.exit(-1) diff --git a/slither/tools/similarity/train.py b/slither/tools/similarity/train.py index ccadf49265..1d84faf2a3 100755 --- a/slither/tools/similarity/train.py +++ b/slither/tools/similarity/train.py @@ -1,4 +1,3 @@ -import argparse import logging import os import sys @@ -11,25 +10,25 @@ logger = logging.getLogger("Slither-simil") -def train(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals +def train(**kwargs) -> None: # pylint: disable=too-many-locals try: last_data_train_filename = "last_data_train.txt" - model_filename = args.model - dirname = args.input + model_filename = kwargs.get("model") + dirname = kwargs.get("input") if dirname is None: logger.error("The train mode requires the input parameter.") sys.exit(-1) - contracts = load_contracts(dirname, **vars(args)) + contracts = load_contracts(dirname, **kwargs) logger.info("Saving extracted data into %s", last_data_train_filename) cache = [] with open(last_data_train_filename, "w", encoding="utf8") as f: for filename in contracts: # cache[filename] = dict() for (filename_inner, contract, function), ir in encode_contract( - filename, **vars(args) + filename, **kwargs ).items(): if ir != []: x = " ".join(ir) @@ -50,6 +49,6 @@ def train(args: argparse.Namespace) -> None: # pylint: disable=too-many-locals logger.info("Done!") except Exception: # pylint: disable=broad-except - logger.error(f"Error in {args.filename}") + logger.error(f"Error in {kwargs.get('filename')}") logger.error(traceback.format_exc()) sys.exit(-1) diff --git a/slither/tools/slither_format/__main__.py b/slither/tools/slither_format/__main__.py index 85c0a3917f..7894d71d31 100644 --- a/slither/tools/slither_format/__main__.py +++ b/slither/tools/slither_format/__main__.py @@ -1,12 +1,20 @@ -import sys -import argparse import logging -from crytic_compile import cryticparser +from typing import Annotated + +import typer + + from slither import Slither -from slither.utils.command_line import read_config_file from slither.tools.slither_format.slither_format import slither_format +from slither.__main__ import app +from slither.utils.command_line import target_type, SlitherState, SlitherApp, GroupWithCrytic + +format_app: SlitherApp = SlitherApp() +app.add_typer(format_app, name="format") + + logging.basicConfig() logging.getLogger("Slither").setLevel(logging.INFO) @@ -19,93 +27,55 @@ "external-function", "constable-states", "constant-function-asm", - "constatnt-function-state", + "constant-function-state", ] -def parse_args() -> argparse.Namespace: - """ - Parse the underlying arguments for the program. - :return: Returns the arguments for the program. - """ - parser = argparse.ArgumentParser(description="slither_format", usage="slither_format filename") - - parser.add_argument( - "filename", help="The filename of the contract or truffle directory to analyze." - ) - parser.add_argument( - "--verbose-test", - "-v", - help="verbose mode output for testing", - action="store_true", - default=False, - ) - parser.add_argument( - "--verbose-json", - "-j", - help="verbose json output", - action="store_true", - default=False, - ) - parser.add_argument( - "--version", - help="displays the current version", - version="0.1.0", - action="version", - ) - - parser.add_argument( - "--config-file", - help="Provide a config file (default: slither.config.json)", - action="store", - dest="config_file", - default="slither.config.json", - ) - - group_detector = parser.add_argument_group("Detectors") - group_detector.add_argument( - "--detect", - help="Comma-separated list of detectors, defaults to all, " - f"available detectors: {', '.join(d for d in available_detectors)}", - action="store", - dest="detectors_to_run", - default="all", - ) - - group_detector.add_argument( - "--exclude", - help="Comma-separated list of detectors to exclude," - "available detectors: {', '.join(d for d in available_detectors)}", - action="store", - dest="detectors_to_exclude", - default="all", - ) - - cryticparser.init(parser) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -def main() -> None: +@format_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + verbose_test: Annotated[ + bool, typer.Option("--verbose-test", "-v", help="Verbose mode output for testing") + ] = False, # Unused? + verbose_json: Annotated[ + bool, typer.Option("--verbose-json", "-j", help="Verbose mode output for testing") + ] = False, # Unused? + detectors_to_run: Annotated[ + str, + typer.Option( + "--detect", + help=f"Comma-separated list of detectors. Available detectors: {', '.join(available_detectors)}", + rich_help_panel="Detectors", + ), + ] = "all", + detectors_to_exclude: Annotated[ + str, + typer.Option( + "--exclude", + help="Comma-separated list of detectors that should be excluded or all.", + rich_help_panel="Detectors", + ), + ] = "", +) -> None: # ------------------------------ # Usage: python3 -m slither_format filename # Example: python3 -m slither_format contract.sol # ------------------------------ - # Parse all arguments - args = parse_args() - - read_config_file(args) # Perform slither analysis on the given filename - slither = Slither(args.filename, **vars(args)) + state = ctx.ensure_object(SlitherState) + slither = Slither(target.target, **state) # Format the input files based on slither analysis - slither_format(slither, **vars(args)) + state.update( + { + "detectors_to_run": detectors_to_run, + "detectors_to_exclude": detectors_to_exclude, + } + ) + slither_format(slither, **state) if __name__ == "__main__": - main() + format_app() diff --git a/slither/tools/upgradeability/__main__.py b/slither/tools/upgradeability/__main__.py index 56b838b9c1..423c84fa55 100644 --- a/slither/tools/upgradeability/__main__.py +++ b/slither/tools/upgradeability/__main__.py @@ -1,18 +1,15 @@ -import argparse import inspect import json import logging -import sys -from typing import List, Any, Type, Dict, Tuple, Union, Sequence, Optional - -from crytic_compile import cryticparser +from typing import List, Type, Dict, Tuple, Union, Optional, Annotated +import typer from slither import Slither from slither.core.declarations import Contract from slither.exceptions import SlitherException from slither.utils.colors import red -from slither.utils.output import output_to_json +from slither.utils.output import output_to_json, OutputFormat from slither.tools.upgradeability.checks import all_checks from slither.tools.upgradeability.checks.abstract_checks import ( AbstractCheck, @@ -22,121 +19,25 @@ output_detectors_json, output_wiki, output_detectors, - output_to_markdown, ) -logging.basicConfig() -logger: logging.Logger = logging.getLogger("Slither") -logger.setLevel(logging.INFO) - - -def parse_args(check_classes: List[Type[AbstractCheck]]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Slither Upgradeability Checks. For usage information see https://github.com/crytic/slither/wiki/Upgradeability-Checks.", - usage="slither-check-upgradeability contract.sol ContractName", - ) - - group_checks = parser.add_argument_group("Checks") - - parser.add_argument("contract.sol", help="Codebase to analyze") - parser.add_argument("ContractName", help="Contract name (logic contract)") - - parser.add_argument("--proxy-name", help="Proxy name") - parser.add_argument("--proxy-filename", help="Proxy filename (if different)") - - parser.add_argument("--new-contract-name", help="New contract name (if changed)") - parser.add_argument( - "--new-contract-filename", help="New implementation filename (if different)" - ) - - parser.add_argument( - "--json", - help='Export the results as a JSON file ("--json -" to export to stdout)', - action="store", - default=False, - ) - - group_checks.add_argument( - "--detect", - help="Comma-separated list of detectors, defaults to all, " - f"available detectors: {', '.join(d.ARGUMENT for d in check_classes)}", - action="store", - dest="detectors_to_run", - default="all", - ) - - group_checks.add_argument( - "--list-detectors", - help="List available detectors", - action=ListDetectors, - nargs=0, - default=False, - ) - - group_checks.add_argument( - "--exclude", - help="Comma-separated list of detectors that should be excluded", - action="store", - dest="detectors_to_exclude", - default=None, - ) - - group_checks.add_argument( - "--exclude-informational", - help="Exclude informational impact analyses", - action="store_true", - default=False, - ) - - group_checks.add_argument( - "--exclude-low", - help="Exclude low impact analyses", - action="store_true", - default=False, - ) - - group_checks.add_argument( - "--exclude-medium", - help="Exclude medium impact analyses", - action="store_true", - default=False, - ) - - group_checks.add_argument( - "--exclude-high", - help="Exclude high impact analyses", - action="store_true", - default=False, - ) - - parser.add_argument( - "--markdown-root", - help="URL for markdown generation", - action="store", - default="", - ) - - parser.add_argument( - "--wiki-detectors", help=argparse.SUPPRESS, action=OutputWiki, default=False - ) - - parser.add_argument( - "--list-detectors-json", - help=argparse.SUPPRESS, - action=ListDetectorsJson, - nargs=0, - default=False, - ) +from slither.__main__ import app +from slither.utils.command_line import ( + target_type, + SlitherState, + SlitherApp, + GroupWithCrytic, + MarkdownRoot, +) - parser.add_argument("--markdown", help=argparse.SUPPRESS, action=OutputMarkdown, default=False) - cryticparser.init(parser) +upgradeability_app: SlitherApp = SlitherApp() +app.add_typer(upgradeability_app, name="check-upgradeability") - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - return parser.parse_args() +logging.basicConfig() +logger: logging.Logger = logging.getLogger("Slither") +logger.setLevel(logging.INFO) ################################################################################### @@ -146,6 +47,17 @@ def parse_args(check_classes: List[Type[AbstractCheck]]) -> argparse.Namespace: ################################################################################### +def list_detectors_json(ctx: typer.Context, value: bool) -> None: + """Action: list detectors in JSON""" + if not value or ctx.resilient_parsing: + return + + checks = _get_checks() + detector_types_json = output_detectors_json(checks) + print(json.dumps(detector_types_json)) + raise typer.Exit(code=0) + + def _get_checks() -> List[Type[AbstractCheck]]: detectors_ = [getattr(all_checks, name) for name in dir(all_checks)] detectors: List[Type[AbstractCheck]] = [ @@ -155,87 +67,66 @@ def _get_checks() -> List[Type[AbstractCheck]]: def choose_checks( - args: argparse.Namespace, all_check_classes: List[Type[AbstractCheck]] + arg_checks_to_run: str, + arg_checks_exclude: str, + exclude_low: bool = False, + exclude_medium: bool = False, + exclude_high: bool = False, + exclude_informational: bool = False, + all_check_classes: List[Type[AbstractCheck]] = None, ) -> List[Type[AbstractCheck]]: - detectors_to_run = [] - detectors = {d.ARGUMENT: d for d in all_check_classes} - - if args.detectors_to_run == "all": - detectors_to_run = all_check_classes - if args.detectors_to_exclude: - detectors_excluded = args.detectors_to_exclude.split(",") - for detector in detectors: - if detector in detectors_excluded: - detectors_to_run.remove(detectors[detector]) + checks_to_run = [] + + if all_check_classes is None: + return [] + + checks = {d.ARGUMENT: d for d in all_check_classes} + + if arg_checks_to_run == "all": + checks_to_run = all_check_classes + if arg_checks_exclude: + checks_excluded = arg_checks_exclude.split(",") + for check in checks: + if check in checks_excluded: + checks_to_run.remove(checks[check]) else: - for detector in args.detectors_to_run.split(","): - if detector in detectors: - detectors_to_run.append(detectors[detector]) + for check in arg_checks_to_run.split(","): + if check in checks: + checks_to_run.append(checks[check]) else: - raise Exception(f"Error: {detector} is not a detector") - detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT) - return detectors_to_run - - if args.exclude_informational: - detectors_to_run = [ - d for d in detectors_to_run if d.IMPACT != CheckClassification.INFORMATIONAL - ] - if args.exclude_low: - detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.LOW] - if args.exclude_medium: - detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.MEDIUM] - if args.exclude_high: - detectors_to_run = [d for d in detectors_to_run if d.IMPACT != CheckClassification.HIGH] + raise Exception(f"Error: {check} is not a detector") + checks_to_run = sorted(checks_to_run, key=lambda x: x.IMPACT) + return checks_to_run + + if exclude_informational: + checks_to_run = [d for d in checks_to_run if d.IMPACT != CheckClassification.INFORMATIONAL] + if exclude_low: + checks_to_run = [d for d in checks_to_run if d.IMPACT != CheckClassification.LOW] + if exclude_medium: + checks_to_run = [d for d in checks_to_run if d.IMPACT != CheckClassification.MEDIUM] + if exclude_high: + checks_to_run = [d for d in checks_to_run if d.IMPACT != CheckClassification.HIGH] # detectors_to_run = sorted(detectors_to_run, key=lambda x: x.IMPACT) - return detectors_to_run - - -class ListDetectors(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - checks = _get_checks() - output_detectors(checks) - parser.exit() - - -class ListDetectorsJson(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, parser: Any, *args: Any, **kwargs: Any - ) -> None: # pylint: disable=signature-differs - checks = _get_checks() - detector_types_json = output_detectors_json(checks) - print(json.dumps(detector_types_json)) - parser.exit() - - -class OutputMarkdown(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, - parser: Any, - args: Any, - values: Optional[Union[str, Sequence[Any]]], - option_string: Any = None, - ) -> None: # pylint: disable=signature-differs - checks = _get_checks() - assert isinstance(values, str) - output_to_markdown(checks, values) - parser.exit() - - -class OutputWiki(argparse.Action): # pylint: disable=too-few-public-methods - def __call__( - self, - parser: Any, - args: Any, - values: Optional[Union[str, Sequence[Any]]], - option_string: Any = None, - ) -> Any: # pylint: disable=signature-differs - checks = _get_checks() - assert isinstance(values, str) - output_wiki(checks, values) - parser.exit() + return checks_to_run + + +def list_detectors_action(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + checks = _get_checks() + output_detectors(checks) + raise typer.Exit() + + +def output_wiki_action(ctx: typer.Context, _: str, value: Union[str, None] = None): + if ctx.resilient_parsing or value is None: + return + + checks = _get_checks() + output_wiki(checks, value) + raise typer.Exit() def _run_checks(detectors: List[AbstractCheck]) -> List[Dict]: @@ -279,31 +170,142 @@ def _checks_on_contract_and_proxy( ################################################################################### ################################################################################### + # pylint: disable=too-many-statements,too-many-branches,too-many-locals -def main() -> None: +@upgradeability_app.callback(cls=GroupWithCrytic) +def main( + ctx: typer.Context, + target: target_type, + contract_name: Annotated[str, typer.Argument(help="Contract name")], + proxy_name: Annotated[Optional[str], typer.Option(help="Proxy name")] = None, + proxy_filename: Annotated[ + Optional[str], typer.Option(help="Proxy filename (if different).") + ] = None, + new_contract_name: Annotated[ + Optional[str], typer.Option(help="New contract name (if changed)") + ] = None, + new_contract_filename: Annotated[ + Optional[str], typer.Option(help="New implementation filename (if different)") + ] = None, + list_json_detector: Annotated[ + Optional[bool], + typer.Option("--list-detectors-json", callback=list_detectors_json, hidden=True), + ] = None, + list_detectors: Annotated[ + Optional[bool], + typer.Option( + "--list-detectors", + help="List available detectors.", + callback=list_detectors_action, + rich_help_panel="Checks", + is_eager=True, + ), + ] = None, + detectors_to_run: Annotated[ + Optional[str], + typer.Option( + "--detect", + help=f"Comma-separated list of detectors. Available detectors: {', '.join(d.ARGUMENT for d in _get_checks())}", + rich_help_panel="Checks", + ), + ] = "all", + detectors_to_exclude: Annotated[ + Optional[str], + typer.Option( + "--exclude", + help="Comma-separated list of detectors that should be excluded or all.", + rich_help_panel="Checks", + ), + ] = "", + exclude_informational: Annotated[ + Optional[bool], + typer.Option( + "--exclude-informational", + help="Exclude informational impact analyses.", + rich_help_panel="Checks", + ), + ] = False, + exclude_low: Annotated[ + Optional[bool], + typer.Option( + "--exclude-low", + help="Exclude low impact analyses.", + rich_help_panel="Checks", + ), + ] = False, + exclude_medium: Annotated[ + Optional[bool], + typer.Option( + "--exclude-medium", + help="Exclude medium impact analyses.", + rich_help_panel="Checks", + ), + ] = False, + exclude_high: Annotated[ + Optional[bool], + typer.Option( + "--exclude-high", + help="Exclude high impact analyses.", + rich_help_panel="Checks", + ), + ] = False, + markdown_root: Annotated[ + Optional[str], + typer.Option( + "--markdown-root", + help="URL for markdown generation.", + rich_help_panel="Check", + click_type=MarkdownRoot(), + ), + ] = None, + wiki_detectors: Annotated[ + Optional[str], + typer.Option( + help="Print each detectors information that matches the pattern.", + callback=output_wiki_action, + rich_help_panel="Check", + ), + ] = None, +) -> None: + """Slither Upgradeability Checks. + + For usage information see https://github.com/crytic/slither/wiki/Upgradeability-Checks. + """ json_results: Dict = { "proxy-present": False, "contract_v2-present": False, "detectors": [], } - detectors = _get_checks() - args = parse_args(detectors) - detectors_to_run = choose_checks(args, detectors) - v1_filename = vars(args)["contract.sol"] + checks = _get_checks() + + detectors_to_run = choose_checks( + arg_checks_to_run=detectors_to_run, + arg_checks_exclude=detectors_to_exclude, + exclude_low=exclude_low, + exclude_medium=exclude_medium, + exclude_high=exclude_high, + exclude_informational=exclude_informational, + all_check_classes=checks, + ) + + v1_filename = target.target + state = ctx.ensure_object(SlitherState) + number_detectors_run = 0 try: - variable1 = Slither(v1_filename, **vars(args)) + variable1 = Slither(v1_filename, **state) # Analyze logic contract - v1_name = args.ContractName + v1_name = contract_name v1_contracts = variable1.get_contract_from_name(v1_name) if len(v1_contracts) != 1: info = f"Contract {v1_name} not found in {variable1.filename}" logger.error(red(info)) - if args.json: - output_to_json(args.json, str(info), json_results) + if state.get("output_format") == OutputFormat.JSON: + output_to_json(state.get("output_file").as_posix(), str(info), json_results) return + v1_contract = v1_contracts[0] detectors_results, number_detectors = _checks_on_contract(detectors_to_run, v1_contract) @@ -312,18 +314,18 @@ def main() -> None: # Analyze Proxy proxy_contract = None - if args.proxy_name: - if args.proxy_filename: - proxy = Slither(args.proxy_filename, **vars(args)) + if proxy_name: + if proxy_filename: + proxy = Slither(proxy_filename, **state) else: proxy = variable1 - proxy_contracts = proxy.get_contract_from_name(args.proxy_name) + proxy_contracts = proxy.get_contract_from_name(proxy_name) if len(proxy_contracts) != 1: - info = f"Proxy {args.proxy_name} not found in {proxy.filename}" + info = f"Proxy {proxy_name} not found in {proxy.filename}" logger.error(red(info)) - if args.json: - output_to_json(args.json, str(info), json_results) + if state.get("output_format") == OutputFormat.JSON: + output_to_json(state.get("output_file").as_posix(), str(info), json_results) return proxy_contract = proxy_contracts[0] json_results["proxy-present"] = True @@ -334,20 +336,18 @@ def main() -> None: json_results["detectors"] += detectors_results number_detectors_run += number_detectors # Analyze new version - if args.new_contract_name: - if args.new_contract_filename: - variable2 = Slither(args.new_contract_filename, **vars(args)) + if new_contract_name: + if new_contract_filename: + variable2 = Slither(new_contract_filename, **state) else: variable2 = variable1 - v2_contracts = variable2.get_contract_from_name(args.new_contract_name) + v2_contracts = variable2.get_contract_from_name(new_contract_name) if len(v2_contracts) != 1: - info = ( - f"New logic contract {args.new_contract_name} not found in {variable2.filename}" - ) + info = f"New logic contract {new_contract_name} not found in {variable2.filename}" logger.error(red(info)) - if args.json: - output_to_json(args.json, str(info), json_results) + if state.get("output_format") == OutputFormat.JSON: + output_to_json(state.get("output_file").as_posix(), str(info), json_results) return v2_contract = v2_contracts[0] json_results["contract_v2-present"] = True @@ -372,13 +372,15 @@ def main() -> None: to_log = f'{len(json_results["detectors"])} findings, {number_detectors_run} detectors run' logger.info(to_log) - if args.json: - output_to_json(args.json, None, json_results) + if state.get("output_format") == OutputFormat.JSON: + output_to_json(state.get("output_file").as_posix(), None, json_results) except SlitherException as slither_exception: logger.error(str(slither_exception)) - if args.json: - output_to_json(args.json, str(slither_exception), json_results) + if state.get("output_format") == OutputFormat.JSON: + output_to_json( + state.get("output_file").as_posix(), str(slither_exception), json_results + ) return diff --git a/slither/utils/codex.py b/slither/utils/codex.py deleted file mode 100644 index 8b15656708..0000000000 --- a/slither/utils/codex.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging -import os -from argparse import ArgumentParser -from pathlib import Path - -from slither.utils.command_line import defaults_flag_in_config - -logger = logging.getLogger("Slither") - - -def init_parser(parser: ArgumentParser, always_enable_codex: bool = False) -> None: - """ - Init the cli arg with codex features - - Args: - parser: - always_enable_codex (Optional(bool)): if true, --codex is not enabled - - Returns: - - """ - group_codex = parser.add_argument_group("Codex (https://beta.openai.com/docs/guides/code)") - - if not always_enable_codex: - group_codex.add_argument( - "--codex", - help="Enable codex (require an OpenAI API Key)", - action="store_true", - default=defaults_flag_in_config["codex"], - ) - - group_codex.add_argument( - "--codex-log", - help="Log codex queries (in crytic_export/codex/)", - action="store_true", - default=False, - ) - - group_codex.add_argument( - "--codex-contracts", - help="Comma separated list of contracts to submit to OpenAI Codex", - action="store", - default=defaults_flag_in_config["codex_contracts"], - ) - - group_codex.add_argument( - "--codex-model", - help="Name of the Codex model to use (affects pricing). Defaults to 'text-davinci-003'", - action="store", - default=defaults_flag_in_config["codex_model"], - ) - - group_codex.add_argument( - "--codex-temperature", - help="Temperature to use with Codex. Lower number indicates a more precise answer while higher numbers return more creative answers. Defaults to 0", - action="store", - default=defaults_flag_in_config["codex_temperature"], - ) - - group_codex.add_argument( - "--codex-max-tokens", - help="Maximum amount of tokens to use on the response. This number plus the size of the prompt can be no larger than the limit (4097 for text-davinci-003)", - action="store", - default=defaults_flag_in_config["codex_max_tokens"], - ) - - group_codex.add_argument( - "--codex-organization", - help="Codex organization", - action="store", - default=None, - ) - - -# TODO: investigate how to set the correct return type -# So that the other modules can work with openai -def openai_module(): # type: ignore - """ - Return the openai module - Consider checking the usage of open (slither.codex_enabled) before using this function - - Returns: - Optional[the openai module] - """ - try: - # pylint: disable=import-outside-toplevel - import openai - - api_key = os.getenv("OPENAI_API_KEY") - if api_key is None: - logger.info( - "Please provide an Open API Key in OPENAI_API_KEY (https://beta.openai.com/account/api-keys)" - ) - return None - openai.api_key = api_key - except ImportError: - logger.info("OpenAI was not installed") # type: ignore - logger.info('run "pip install openai"') - return None - return openai - - -def log_codex(filename: str, prompt: str) -> None: - """ - Log the prompt in crytic/export/codex/filename - Append to the file - - Args: - filename: filename to write to - prompt: prompt to write - - Returns: - None - """ - - Path("crytic_export/codex").mkdir(parents=True, exist_ok=True) - - with open(Path("crytic_export/codex", filename), "a", encoding="utf8") as file: - file.write(prompt) - file.write("\n") diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py index a37e859133..f68be7b39b 100644 --- a/slither/utils/command_line.py +++ b/slither/utils/command_line.py @@ -1,23 +1,45 @@ import argparse +import cProfile import enum import json import os +import pstats import re import logging -from collections import defaultdict -from typing import Dict, List, Type, Union - +import sys +import textwrap +from dataclasses import dataclass +from functools import lru_cache +from importlib import metadata +from pathlib import Path +from typing import Dict, List, Union, Any, Optional, Callable, Sequence, Tuple + +import typer.core +from click import Context +from typer.models import CommandFunctionType +from typing_extensions import Annotated + +import click +import typer + +from crytic_compile import cryticparser from crytic_compile.cryticparser.defaults import ( DEFAULTS_FLAG_IN_CONFIG as DEFAULTS_FLAG_IN_CONFIG_CRYTIC_COMPILE, ) +from crytic_compile.platform.etherscan import SUPPORTED_NETWORK -from slither.detectors.abstract_detector import classification_txt, AbstractDetector -from slither.printers.abstract_printer import AbstractPrinter from slither.utils.colors import yellow, red -from slither.utils.myprettytable import MyPrettyTable +from slither.utils.output import ZipType logger = logging.getLogger("Slither") + +class SlitherState(dict): + """Used to keep the internal state of the application.""" + + pass + + DEFAULT_JSON_OUTPUT_TYPES = ["detectors", "printers"] JSON_OUTPUT_TYPES = [ "compilations", @@ -37,6 +59,198 @@ class FailOnLevel(enum.Enum): NONE = "none" +class GroupWithCrytic(typer.core.TyperGroup): + def invoke(self, ctx: Context) -> Any: + if ctx.args or ctx.protected_args: + # If we have additional parameters to parse with crytic, they will be stored here. + # We could have a better solution when this issue is solved in Typer + # https://github.com/tiangolo/typer/issues/119 + crytic_args, remaining_args = handle_crytic_args( + ctx.protected_args + ctx.args, no_error=True + ) + + # Remove all handled arguments from the context + ctx.protected_args = [arg for arg in ctx.protected_args if arg in remaining_args] + ctx.args = [arg for arg in ctx.args if arg in remaining_args] + + if crytic_args: + state = ctx.ensure_object(SlitherState) + state.update(crytic_args) + + return super().invoke(ctx) + + +class CommandWithCrytic(typer.core.TyperCommand): + """Command that allow crytic compile arguments.""" + + def invoke(self, ctx: Context) -> Any: + """Command invocation + + Before invoking the command, handle any crytic parameters passed on the command line. + """ + if ctx.args: + crytic_args, _ = handle_crytic_args(ctx.args) + + if crytic_args: + state = ctx.ensure_object(SlitherState) + state.update(crytic_args) + + return super().invoke(ctx) + + +class SlitherApp(typer.Typer): + def __init__(self, default_method: Union[None, str] = None, *args, **kwargs) -> None: + super().__init__(*args, no_args_is_help=True, **kwargs) + self.default_method: Union[None, str] = default_method + + @property + @lru_cache + def click_app(self) -> Union[GroupWithCrytic, click.Command]: + return typer.main.get_command(self) + + @property + def app_commands(self) -> Sequence[str]: + """Return the app registered commands if we have any.""" + + try: + return self.click_app.commands.keys() + except AttributeError: + return [] + + def command(self, *args, **kwargs) -> Callable[[CommandFunctionType], CommandFunctionType]: + """Passthrough command to allow extra options. + + This is used to parse crytic compile arguments. + """ + context_settings = kwargs.get("context_settings", {}) + context_settings.update( + { + "ignore_unknown_options": True, + "allow_extra_args": True, + } + ) + + return super().command( + *args, context_settings=context_settings, cls=CommandWithCrytic, **kwargs + ) + + def callback(self, *args, **kwargs) -> Callable[[CommandFunctionType], CommandFunctionType]: + """A modified version of the callback accepting extra options tailored for Slither usage.""" + context_settings = kwargs.get("context_settings", {}) + context_settings.update( + { + "ignore_unknown_options": True, + "allow_extra_args": True, + } + ) + + kwargs["context_settings"] = context_settings + kwargs["invoke_without_command"] = kwargs.get("invoke_without_command", True) + + return super().callback(*args, **kwargs) + + def original_command( + self, *args, **kwargs + ) -> Callable[[CommandFunctionType], CommandFunctionType]: + """Direct wrapper of the original command.""" + return super().command(*args, **kwargs) + + def original_callback( + self, *args, **kwargs + ) -> Callable[[CommandFunctionType], CommandFunctionType]: + """Direct wrapper to the original callback commnand.""" + return super().callback(*args, **kwargs) + + def __call__(self, *args, **kwargs): + """Overrides Typer command handling. + + We override the calling mechanism here to allow a smooth transition from the previous CLI + interface to the new sub command based one. + """ + + if self.default_method is not None and not any( + command in sys.argv for command in self.app_commands + ): + + main_command_args = [] + other_command_args = [] + + take_next: bool = False + main_args = {option: param for param in self.click_app.params for option in param.opts} + + command_args = sys.argv[1:] + for idx, arg in enumerate(command_args): + if take_next: + take_next = False + continue + + # The help command is not listed in the main_args directly, so we consider it + # ourselves + if arg == "--help": + main_command_args.append(arg) + continue + + if arg in main_args: + main_command_args.append(arg) + if not main_args[arg].is_flag: + main_command_args.append(command_args[idx + 1]) + take_next = True + else: + other_command_args.append(arg) + + if other_command_args: + sys.argv = [ + sys.argv[0], + *main_command_args, + self.default_method, + *other_command_args, + ] + logger.info( + f"Deprecation Notice: Slither CLI has moved to a subcommand based interface. " + f"This command has been automatically transposed to: %s", + " ".join(sys.argv), + ) + + super().__call__(*args, **kwargs) + + return + + +@click.pass_context +def slither_end_callback(ctx: click.Context, *args, **kwargs) -> None: + """End execution callback.""" + + # If we have asked for the perf object + ctx.state = ctx.ensure_object(SlitherState) + perf: Union[cProfile.Profile, None] = ctx.state.get("perf", None) + if perf is not None: + perf.disable() + stats = pstats.Stats(perf).sort_stats("cumtime") + stats.print_stats() + + +def format_crytic_help(ctx: typer.Context): + """""" + parser = argparse.ArgumentParser() + cryticparser.init(parser) + + for action_group in parser._action_groups: + + if action_group.title in ("positional arguments", "options"): + continue + + for action in action_group._group_actions: + param = click.Option( + action.option_strings, + help=action.help, + hidden=False, # TODO(dm) + show_default=False, + ) + param.rich_help_panel = action_group.title + + ctx.command.params.append(param) + + # Those are the flags shared by the command line and the config file defaults_flag_in_config = { "codex": False, @@ -66,7 +280,7 @@ class FailOnLevel(enum.Enum): "skip_assembly": False, "legacy_ast": False, "zip": None, - "zip_type": "lzma", + "zip_type": ZipType.LZMA, "show_ignored_findings": False, "no_fail": False, "sarif_input": "export.sarif", @@ -76,319 +290,62 @@ class FailOnLevel(enum.Enum): } -def read_config_file(args: argparse.Namespace) -> None: - # No config file was provided as an argument - if args.config_file is None: - # Check wether the default config file is present - if os.path.exists("slither.config.json"): - # The default file exists, use it - args.config_file = "slither.config.json" - else: - return +def read_config_file(config_file: Union[None, Path]) -> Dict[str, Any]: + if config_file is None: + config_path = Path("slither.config.json") + else: + config_path = config_file - if os.path.isfile(args.config_file): - try: - with open(args.config_file, encoding="utf8") as f: + state: Dict[str, Any] = defaults_flag_in_config + if config_path.is_file(): + + with config_path.open(encoding="utf-8") as f: + try: config = json.load(f) - for key, elem in config.items(): - if key not in defaults_flag_in_config: - logger.info( - yellow(f"{args.config_file} has an unknown key: {key} : {elem}") - ) - continue - if getattr(args, key) == defaults_flag_in_config[key]: - setattr(args, key, elem) - except json.decoder.JSONDecodeError as e: - logger.error(red(f"Impossible to read {args.config_file}, please check the file {e}")) - else: - logger.error(red(f"File {args.config_file} is not a file or does not exist")) + except json.JSONDecodeError as exc: + logger.error(red(f"Impossible to read {config_file}, please check the file {exc}")) + + for key, elem in config.items(): + if key not in defaults_flag_in_config: + logger.info(yellow(f"{config_file} has an unknown key: {key} : {elem}")) + continue + + state[key] = elem + + elif config_file is not None: + logger.error(red(f"File {config_file} is not a file or does not exist")) logger.error(yellow("Falling back to the default settings...")) + return state -def output_to_markdown( - detector_classes: List[Type[AbstractDetector]], - printer_classes: List[Type[AbstractPrinter]], - filter_wiki: str, -) -> None: - def extract_help(cls: Union[Type[AbstractDetector], Type[AbstractPrinter]]) -> str: - if cls.WIKI == "": - return cls.HELP - return f"[{cls.HELP}]({cls.WIKI})" - - detectors_list = [] - print(filter_wiki) - for detector in detector_classes: - argument = detector.ARGUMENT - # dont show the backdoor example - if argument == "backdoor": - continue - if not filter_wiki in detector.WIKI: - continue - help_info = extract_help(detector) - impact = detector.IMPACT - confidence = classification_txt[detector.CONFIDENCE] - detectors_list.append((argument, help_info, impact, confidence)) - - # Sort by impact, confidence, and name - detectors_list = sorted( - detectors_list, key=lambda element: (element[2], element[3], element[0]) - ) - idx = 1 - for (argument, help_info, impact, confidence) in detectors_list: - print(f"{idx} | `{argument}` | {help_info} | {classification_txt[impact]} | {confidence}") - idx = idx + 1 - - print() - printers_list = [] - for printer in printer_classes: - argument = printer.ARGUMENT - help_info = extract_help(printer) - printers_list.append((argument, help_info)) - - # Sort by impact, confidence, and name - printers_list = sorted(printers_list, key=lambda element: (element[0])) - idx = 1 - for (argument, help_info) in printers_list: - print(f"{idx} | `{argument}` | {help_info}") - idx = idx + 1 - - -def get_level(l: str) -> int: - tab = l.count("\t") + 1 - if l.replace("\t", "").startswith(" -"): - tab = tab + 1 - if l.replace("\t", "").startswith("-"): - tab = tab + 1 - return tab - - -def convert_result_to_markdown(txt: str) -> str: - # -1 to remove the last \n - lines = txt[0:-1].split("\n") - ret = [] - level = 0 - for l in lines: - next_level = get_level(l) - prefix = "
  • " - if next_level < level: - prefix = "" * (level - next_level) + prefix - if next_level > level: - prefix = "
      " * (next_level - level) + prefix - level = next_level - ret.append(prefix + l) - - return "".join(ret) - - -def output_results_to_markdown( - all_results: List[Dict], checklistlimit: str, show_ignored_findings: bool -) -> None: - checks = defaultdict(list) - info: Dict = defaultdict(dict) - for results_ in all_results: - checks[results_["check"]].append(results_) - info[results_["check"]] = { - "impact": results_["impact"], - "confidence": results_["confidence"], - } - - if not show_ignored_findings: - print( - "**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results." - ) - print("Summary") - for check_ in checks: - print( - f" - [{check_}](#{check_}) ({len(checks[check_])} results) ({info[check_]['impact']})" - ) +def version_callback(ctx: typer.Context, value: bool) -> None: + """Callback called when the --version flag is used.""" + if not value or ctx.resilient_parsing: + return - counter = 0 - for (check, results) in checks.items(): - print(f"## {check}") - print(f'Impact: {info[check]["impact"]}') - print(f'Confidence: {info[check]["confidence"]}') - additional = False - if checklistlimit and len(results) > 5: - results = results[0:5] - additional = True - for result in results: - print(" - [ ] ID-" + f"{counter}") - counter = counter + 1 - print(result["markdown"]) - if result["first_markdown_element"]: - print(result["first_markdown_element"]) - print("\n") - if additional: - print(f"**More results were found, check [{checklistlimit}]({checklistlimit})**") - - -def output_wiki(detector_classes: List[Type[AbstractDetector]], filter_wiki: str) -> None: - - # Sort by impact, confidence, and name - detectors_list = sorted( - detector_classes, - key=lambda element: (element.IMPACT, element.CONFIDENCE, element.ARGUMENT), - ) + print(metadata.version("slither-analyzer")) + raise typer.Exit(code=0) - for detector in detectors_list: - argument = detector.ARGUMENT - # dont show the backdoor example - if argument == "backdoor": - continue - if not filter_wiki in detector.WIKI: - continue - check = detector.ARGUMENT - impact = classification_txt[detector.IMPACT] - confidence = classification_txt[detector.CONFIDENCE] - title = detector.WIKI_TITLE - description = detector.WIKI_DESCRIPTION - exploit_scenario = detector.WIKI_EXPLOIT_SCENARIO - recommendation = detector.WIKI_RECOMMENDATION - - print(f"\n## {title}") - print("### Configuration") - print(f"* Check: `{check}`") - print(f"* Severity: `{impact}`") - print(f"* Confidence: `{confidence}`") - print("\n### Description") - print(description) - if exploit_scenario: - print("\n### Exploit Scenario:") - print(exploit_scenario) - print("\n### Recommendation") - print(recommendation) - - -def output_detectors(detector_classes: List[Type[AbstractDetector]]) -> None: - detectors_list = [] - for detector in detector_classes: - argument = detector.ARGUMENT - # dont show the backdoor example - if argument == "backdoor": - continue - help_info = detector.HELP - impact = detector.IMPACT - confidence = classification_txt[detector.CONFIDENCE] - detectors_list.append((argument, help_info, impact, confidence)) - table = MyPrettyTable(["Num", "Check", "What it Detects", "Impact", "Confidence"]) - - # Sort by impact, confidence, and name - detectors_list = sorted( - detectors_list, key=lambda element: (element[2], element[3], element[0]) - ) - idx = 1 - for (argument, help_info, impact, confidence) in detectors_list: - table.add_row([str(idx), argument, help_info, classification_txt[impact], confidence]) - idx = idx + 1 - print(table) - - -# pylint: disable=too-many-locals -def output_detectors_json( - detector_classes: List[Type[AbstractDetector]], -) -> List[Dict]: - detectors_list = [] - for detector in detector_classes: - argument = detector.ARGUMENT - # dont show the backdoor example - if argument == "backdoor": - continue - help_info = detector.HELP - impact = detector.IMPACT - confidence = classification_txt[detector.CONFIDENCE] - wiki_url = detector.WIKI - wiki_description = detector.WIKI_DESCRIPTION - wiki_exploit_scenario = detector.WIKI_EXPLOIT_SCENARIO - wiki_recommendation = detector.WIKI_RECOMMENDATION - detectors_list.append( - ( - argument, - help_info, - impact, - confidence, - wiki_url, - wiki_description, - wiki_exploit_scenario, - wiki_recommendation, - ) - ) - # Sort by impact, confidence, and name - detectors_list = sorted( - detectors_list, key=lambda element: (element[2], element[3], element[0]) - ) - idx = 1 - table = [] - for ( - argument, - help_info, - impact, - confidence, - wiki_url, - description, - exploit, - recommendation, - ) in detectors_list: - table.append( - { - "index": idx, - "check": argument, - "title": help_info, - "impact": classification_txt[impact], - "confidence": confidence, - "wiki_url": wiki_url, - "description": description, - "exploit_scenario": exploit, - "recommendation": recommendation, - } +class MarkdownRoot(click.ParamType): + """Type definition for MarkdownRoot.""" + + name = "MarkdownRoot" + + def convert(self, markdown_root: Union[None, str], param, ctx) -> Union[str, None]: + """Convert and validates the markdown root option""" + if markdown_root is None or ctx.resilient_parsing: + return + + # Regex to check whether the markdown_root is a GitHub URL + match = re.search( + r"(https://)github.com/([a-zA-Z-]+)([:/][A-Za-z0-9_.-]+[:/]?)([A-Za-z0-9_.-]*)(.*)", + markdown_root, ) - idx = idx + 1 - return table - - -def output_printers(printer_classes: List[Type[AbstractPrinter]]) -> None: - printers_list = [] - for printer in printer_classes: - argument = printer.ARGUMENT - help_info = printer.HELP - printers_list.append((argument, help_info)) - table = MyPrettyTable(["Num", "Printer", "What it Does"]) - - # Sort by impact, confidence, and name - printers_list = sorted(printers_list, key=lambda element: (element[0])) - idx = 1 - for (argument, help_info) in printers_list: - table.add_row([str(idx), argument, help_info]) - idx = idx + 1 - print(table) - - -def output_printers_json(printer_classes: List[Type[AbstractPrinter]]) -> List[Dict]: - printers_list = [] - for printer in printer_classes: - argument = printer.ARGUMENT - help_info = printer.HELP - - printers_list.append((argument, help_info)) - - # Sort by name - printers_list = sorted(printers_list, key=lambda element: (element[0])) - idx = 1 - table = [] - for (argument, help_info) in printers_list: - table.append({"index": idx, "check": argument, "title": help_info}) - idx = idx + 1 - return table - - -def check_and_sanitize_markdown_root(markdown_root: str) -> str: - # Regex to check whether the markdown_root is a GitHub URL - match = re.search( - r"(https://)github.com/([a-zA-Z-]+)([:/][A-Za-z0-9_.-]+[:/]?)([A-Za-z0-9_.-]*)(.*)", - markdown_root, - ) - if match: + if not match: + self.fail(f"{markdown_root!r} is invalid.", param, ctx) + if markdown_root[-1] != "/": logger.warning("Appending '/' in markdown_root url for better code referencing") markdown_root = markdown_root + "/" @@ -405,4 +362,82 @@ def check_and_sanitize_markdown_root(markdown_root: str) -> str: positions = match.span(4) markdown_root = f"{markdown_root[:positions[0]]}blob{markdown_root[positions[1]:]}" - return markdown_root + return markdown_root + + +class CommaSeparatedValueParser(click.ParamType): + name = "CommaSeparatedValue" + + help = "A comma-separated list of values." + + def convert(self, value: Union[str, None], param, ctx) -> List[str]: + if value is None or ctx.resilient_parsing: + return [] + + paths = value.split(",") + return paths + + +def long_help(ctx: typer.Context, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + format_crytic_help(ctx) + ctx.get_help() + raise typer.Exit() + + +def handle_crytic_args(args: List[str], no_error: bool = False) -> Tuple[Dict[str, str], List[str]]: + """Handle the crytic arguments passed and not handled by slither.""" + + crytic_args = {} + if args: + parser = argparse.ArgumentParser(allow_abbrev=False, add_help=False) + cryticparser.init(parser) + + crytic_args, remaining_args = parser.parse_known_args(args) + arg_mapping = {key: value for key, value in vars(crytic_args).items()} + if remaining_args and not no_error: + msg = "Unknown arguments: %s" % remaining_args + raise typer.BadParameter(msg) + + return arg_mapping, remaining_args + + return crytic_args, [] + + +@dataclass +class Target: + target: Union[str, Path] + + HELP: str = textwrap.dedent( + f""" + Target can be: + + - *file.sol*: a Solidity file + + - *project_directory*: a project directory. + See the [documentation](https://github.com/crytic/crytic-compile/#crytic-compile) for the supported + platforms. + + - *0x..* : a contract address on mainnet + + - *NETWORK:0x..*: a contract on a different network. + Supported networks: {', '.join(x[:-1] for x in SUPPORTED_NETWORK)} + """ + ) + + +class TargetParam(click.ParamType): + name = "Target" + + def convert(self, value: Union[str, Path], param, ctx) -> Union[Target, None]: + if ctx.resilient_parsing: + return None + + return Target(value) + + +target_type = Annotated[ + Optional[Target], typer.Argument(..., help=Target.HELP, click_type=TargetParam()) +] diff --git a/slither/utils/output.py b/slither/utils/output.py index 176b250e3a..9d5c228c59 100644 --- a/slither/utils/output.py +++ b/slither/utils/output.py @@ -1,13 +1,16 @@ +from enum import Enum as PythonEnum import hashlib import json import logging import os import zipfile -from collections import OrderedDict +from collections import OrderedDict, defaultdict from importlib import metadata +from pathlib import Path from typing import Tuple, Optional, Dict, List, Union, Any, TYPE_CHECKING, Type from zipfile import ZipFile +from crytic_compile.platform.standard import generate_standard_export from slither.core.cfg.node import Node from slither.core.declarations import ( @@ -26,9 +29,11 @@ from slither.exceptions import SlitherError from slither.utils.colors import yellow from slither.utils.myprettytable import MyPrettyTable +from slither.utils.output_capture import StandardOutputCapture if TYPE_CHECKING: from slither.core.compilation_unit import SlitherCompilationUnit + from slither.printers.abstract_printer import AbstractPrinter from slither.detectors.abstract_detector import AbstractDetector logger = logging.getLogger("Slither") @@ -189,6 +194,28 @@ def output_to_sarif( json.dump(sarif, f, indent=2) +class ZipType(str, PythonEnum): + LZMA = "lzma" + STORED = "stored" + DEFLATED = "deflated" + BZIP2 = "bzip2" + + @classmethod + def get_zip_type(cls, value: "ZipType"): + mapping = { + cls.LZMA: zipfile.ZIP_LZMA, + cls.STORED: zipfile.ZIP_STORED, + cls.DEFLATED: zipfile.ZIP_DEFLATED, + cls.BZIP2: zipfile.ZIP_BZIP2, + } + + try: + return mapping[value] + except ValueError as exc: + msg = f"Invalid zip type: {value}" + raise ValueError(msg) from exc + + # https://docs.python.org/3/library/zipfile.html#zipfile-objects ZIP_TYPES_ACCEPTED = { "lzma": zipfile.ZIP_LZMA, @@ -744,3 +771,352 @@ def add_other( # Create the underlying element and add it to our resulting json element = _create_base_element("other", name, source_mapping, {}, additional_fields) self._data["elements"].append(element) + + +def output_results_to_markdown( + all_results: List[Dict], checklistlimit: int, show_ignored_findings: bool +) -> None: + checks = defaultdict(list) + info: Dict = defaultdict(dict) + for results_ in all_results: + checks[results_["check"]].append(results_) + info[results_["check"]] = { + "impact": results_["impact"], + "confidence": results_["confidence"], + } + + if not show_ignored_findings: + print( + "**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results." + ) + + print("Summary") + for check_ in checks: + print( + f" - [{check_}](#{check_}) ({len(checks[check_])} results) ({info[check_]['impact']})" + ) + + counter = 0 + for (check, results) in checks.items(): + print(f"## {check}") + print(f'Impact: {info[check]["impact"]}') + print(f'Confidence: {info[check]["confidence"]}') + additional = False + if checklistlimit and len(results) > 5: + results = results[0:5] + additional = True + for result in results: + print(" - [ ] ID-" + f"{counter}") + counter = counter + 1 + print(result["markdown"]) + if result["first_markdown_element"]: + print(result["first_markdown_element"]) + print("\n") + if additional: + print(f"**More results were found, check [{checklistlimit}]({checklistlimit})**") + + +class OutputFormat(str, PythonEnum): + TEXT = "text" + JSON = "json" + SARIF = "sarif" + ZIP = "zip" + + +# pylint: disable=too-many-arguments,too-many-locals +def format_output( + output_format: OutputFormat, + output_file: Path, + slither_instances, + results_detectors, + results_printers, + output_error, + runned_detectors: Union[List[Type["AbstractDetector"]], None] = None, + json_types: Union[None, List[str]] = None, + zip_type: ZipType = ZipType.LZMA, + checklist: bool = False, + checklist_limit: int = 1, + show_ignored_findings: bool = False, + all_detectors: Union[None, List[Type["AbstractDetector"]]] = None, + all_printers: Union[None, List[Type["AbstractPrinter"]]] = None, +): + if output_format in (OutputFormat.JSON, OutputFormat.SARIF, OutputFormat.ZIP): + json_results: Dict[str, Any] = {} + + if "compilation" in json_types: + compilation_results = [] + for slither_instance in slither_instances: + assert slither_instance.crytic_compile + compilation_results.append( + generate_standard_export(slither_instance.crytic_compile) + ) + json_results["compilations"] = compilation_results + + # Add our detector results to JSON if desired. + if results_detectors and "detectors" in json_types: + json_results["detectors"] = results_detectors + + # Add our printer results to JSON if desired. + if results_printers and "printers" in json_types: + json_results["printers"] = results_printers + + # Add our detector types to JSON + if "list-detectors" in json_types: + json_results["list-detectors"] = output_detectors_json(all_detectors) + + # Add our detector types to JSON + if "list-printers" in json_types: + json_results["list-printers"] = output_printers_json(all_printers) + + if output_format == OutputFormat.JSON: + if "console" in json_types: + json_results["console"] = { + "stdout": StandardOutputCapture.get_stdout_output(), + "stderr": StandardOutputCapture.get_stderr_output(), + } + StandardOutputCapture.disable() + output_to_json(output_file.as_posix(), output_error, json_results) + elif output_format == OutputFormat.SARIF: + StandardOutputCapture.disable() + output_to_sarif(output_file.as_posix(), json_results, runned_detectors) + elif output_format == OutputFormat.ZIP: + output_to_zip(output_file.as_posix(), output_error, json_results, zip_type.value) + + elif checklist is True: + output_results_to_markdown( + results_detectors, + checklist_limit, + show_ignored_findings, + ) + + +def output_to_markdown( + detector_classes: List[Type["AbstractDetector"]], + printer_classes: List[Type["AbstractPrinter"]], + filter_wiki: str, +) -> None: + def extract_help(cls: Union[Type["AbstractDetector"], Type["AbstractPrinter"]]) -> str: + if cls.WIKI == "": + return cls.HELP + return f"[{cls.HELP}]({cls.WIKI})" + + detectors_list = [] + print(filter_wiki) + for detector in detector_classes: + argument = detector.ARGUMENT + # dont show the backdoor example + if argument == "backdoor": + continue + if not filter_wiki in detector.WIKI: + continue + help_info = extract_help(detector) + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + detectors_list.append((argument, help_info, impact, confidence)) + + # Sort by impact, confidence, and name + detectors_list = sorted( + detectors_list, key=lambda element: (element[2], element[3], element[0]) + ) + idx = 1 + for (argument, help_info, impact, confidence) in detectors_list: + print(f"{idx} | `{argument}` | {help_info} | {classification_txt[impact]} | {confidence}") + idx = idx + 1 + + print() + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = extract_help(printer) + printers_list.append((argument, help_info)) + + # Sort by impact, confidence, and name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + print(f"{idx} | `{argument}` | {help_info}") + idx = idx + 1 + + +def convert_result_to_markdown(txt: str) -> str: + def get_level(l: str) -> int: + tab = l.count("\t") + 1 + if l.replace("\t", "").startswith(" -"): + tab = tab + 1 + if l.replace("\t", "").startswith("-"): + tab = tab + 1 + return tab + + # -1 to remove the last \n + lines = txt[0:-1].split("\n") + ret = [] + level = 0 + for l in lines: + next_level = get_level(l) + prefix = "
    • " + if next_level < level: + prefix = "
    " * (level - next_level) + prefix + if next_level > level: + prefix = "
      " * (next_level - level) + prefix + level = next_level + ret.append(prefix + l) + + return "".join(ret) + + +def output_wiki(detector_classes: List[Type["AbstractDetector"]], filter_wiki: str) -> None: + + # Sort by impact, confidence, and name + detectors_list = sorted( + detector_classes, + key=lambda element: (element.IMPACT, element.CONFIDENCE, element.ARGUMENT), + ) + + for detector in detectors_list: + argument = detector.ARGUMENT + # dont show the backdoor example + if argument == "backdoor": + continue + if not filter_wiki in detector.WIKI: + continue + check = detector.ARGUMENT + impact = classification_txt[detector.IMPACT] + confidence = classification_txt[detector.CONFIDENCE] + title = detector.WIKI_TITLE + description = detector.WIKI_DESCRIPTION + exploit_scenario = detector.WIKI_EXPLOIT_SCENARIO + recommendation = detector.WIKI_RECOMMENDATION + + print(f"\n## {title}") + print("### Configuration") + print(f"* Check: `{check}`") + print(f"* Severity: `{impact}`") + print(f"* Confidence: `{confidence}`") + print("\n### Description") + print(description) + if exploit_scenario: + print("\n### Exploit Scenario:") + print(exploit_scenario) + print("\n### Recommendation") + print(recommendation) + + +def output_detectors(detector_classes: List[Type["AbstractDetector"]]) -> None: + detectors_list = [] + for detector in detector_classes: + argument = detector.ARGUMENT + # dont show the backdoor example + if argument == "backdoor": + continue + help_info = detector.HELP + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + detectors_list.append((argument, help_info, impact, confidence)) + table = MyPrettyTable(["Num", "Check", "What it Detects", "Impact", "Confidence"]) + + # Sort by impact, confidence, and name + detectors_list = sorted( + detectors_list, key=lambda element: (element[2], element[3], element[0]) + ) + idx = 1 + for (argument, help_info, impact, confidence) in detectors_list: + table.add_row([str(idx), argument, help_info, classification_txt[impact], confidence]) + idx = idx + 1 + print(table) + + +def output_detectors_json( + detector_classes: List[Type["AbstractDetector"]], +) -> List[Dict]: + detectors_list = [] + for detector in detector_classes: + argument = detector.ARGUMENT + # dont show the backdoor example + if argument == "backdoor": + continue + help_info = detector.HELP + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + wiki_url = detector.WIKI + wiki_description = detector.WIKI_DESCRIPTION + wiki_exploit_scenario = detector.WIKI_EXPLOIT_SCENARIO + wiki_recommendation = detector.WIKI_RECOMMENDATION + detectors_list.append( + ( + argument, + help_info, + impact, + confidence, + wiki_url, + wiki_description, + wiki_exploit_scenario, + wiki_recommendation, + ) + ) + + # Sort by impact, confidence, and name + detectors_list = sorted( + detectors_list, key=lambda element: (element[2], element[3], element[0]) + ) + idx = 1 + table = [] + for ( + argument, + help_info, + impact, + confidence, + wiki_url, + description, + exploit, + recommendation, + ) in detectors_list: + table.append( + { + "index": idx, + "check": argument, + "title": help_info, + "impact": classification_txt[impact], + "confidence": confidence, + "wiki_url": wiki_url, + "description": description, + "exploit_scenario": exploit, + "recommendation": recommendation, + } + ) + idx = idx + 1 + return table + + +def output_printers(printer_classes: List[Type["AbstractPrinter"]]) -> None: + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + table = MyPrettyTable(["Num", "Printer", "What it Does"]) + + # Sort by impact, confidence, and name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + table.add_row([str(idx), argument, help_info]) + idx = idx + 1 + print(table) + + +def output_printers_json(printer_classes: List[Type["AbstractPrinter"]]) -> List[Dict]: + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + + printers_list.append((argument, help_info)) + + # Sort by name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + table = [] + for (argument, help_info) in printers_list: + table.append({"index": idx, "check": argument, "title": help_info}) + idx = idx + 1 + return table diff --git a/tests/e2e/cli/__init__.py b/tests/e2e/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/cli/test_cli.py b/tests/e2e/cli/test_cli.py new file mode 100644 index 0000000000..1e13f25648 --- /dev/null +++ b/tests/e2e/cli/test_cli.py @@ -0,0 +1,11 @@ +from typing import Optional, Union, Sequence, IO, Any, Mapping +from typer.testing import CliRunner +from slither.__main__ import app + + +def test_app(): + runner = CliRunner() + + result = runner.invoke(app, ["--help"]) + + assert result.exit_code == 0