From f6ed3ba68d1a152c5c0e9172b631157308b80464 Mon Sep 17 00:00:00 2001 From: redshark1802 Date: Mon, 22 Oct 2018 21:43:17 +0200 Subject: [PATCH 1/2] cli improvements - help is printed when no arguments are supplied - rework detector arguments: detectors are now a comma-separated list, defaults to all - rework printer arguments: printers are now a comma-separated list, defaults to contract-summary - add version command - add --list-detectors and --list-printers - update README --- README.md | 51 +++++++------ scripts/travis_test.sh | 30 ++++---- slither/__main__.py | 167 +++++++++++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 03c8712b3..dd26c2e98 100644 --- a/README.md +++ b/README.md @@ -36,32 +36,39 @@ If Slither is run on a directory, it will run on every `.sol` file of the direct ### Printers -* `--printer-summary`: Print a summary of the contracts -* `--printer-quick-summary`: Print a quick summary of the contracts -* `--printer-inheritance`: Print the inheritance relations -* `--printer-inheritance-graph`: Print the inheritance graph in a file -* `--printer-vars-and-auth`: Print the variables written and the check on `msg.sender` of each function +By default, the `contract-summary` printer is used. Use --printers comma-separated list of printers, +or `none` to disable the default printer. -## Checks available +Num | Printer | Description +--- | --- | --- +1 | `contract-summary` | a summary of the contract +2 | `function-summary` | the summary of the functions +3 | `inheritance` | the inheritance relation between contracts +4 | `inheritance-graph` | the inheritance graph +5 | `slithir` | the slithIR +6 | `vars-and-auth` | the state variables written and the authorization of the functions -By default, all the checks are run. Use --detect-_name-of-check_ to run one check at a time. +## Detectors -Num | Check | What it Detects | Impact | Confidence +By default, all the detectors are run. Use --detectors comma-separated list of detectors to run. + +Num | Detector | What it Detects | Impact | Confidence --- | --- | --- | --- | --- -1 | `suicidal` | Suicidal functions | High | High -2 | `uninitialized-state` | Uninitialized state variables | High | High -3 | `uninitialized-storage` | Uninitialized storage variables | High | High -4 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium -5 | `reentrancy` | Reentrancy vulnerabilities | High | Medium -6 | `locked-ether` | Contracts that lock ether | Medium | High -7 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium -8 | `assembly` | Assembly usage | Informational | High -9 | `const-candidates-state` | State variables that could be declared constant | Informational | High -10 | `low-level-calls` | Low level calls | Informational | High -11 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High -12 | `pragma` | If different pragma directives are used | Informational | High -13 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High -14 | `unused-state` | Unused state variables | Informational | High +1 | `backdoor` | Function named backdoor (detector example) | High | High +2 | `suicidal` | Suicidal functions | High | High +3 | `uninitialized-state` | Uninitialized state variables | High | High +4 | `uninitialized-storage` | Uninitialized storage variables | High | High +5 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium +6 | `reentrancy` | Reentrancy vulnerabilities | High | Medium +7 | `locked-ether` | Contracts that lock ether | Medium | High +8 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium +9 | `assembly` | Assembly usage | Informational | High +10 | `const-candidates-state` | State variables that could be declared constant | Informational | High +11 | `low-level-calls` | Low level calls | Informational | High +12 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High +13 | `pragma` | If different pragma directives are used | Informational | High +14 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High +15 | `unused-state` | Unused state variables | Informational | High [Contact us](https://www.trailofbits.com/contact/) to get access to additional detectors. diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh index 0e4e4030a..0a8db8644 100755 --- a/scripts/travis_test.sh +++ b/scripts/travis_test.sh @@ -3,79 +3,79 @@ ### Test Detectors -slither tests/uninitialized.sol --disable-solc-warnings --detect-uninitialized-state +slither tests/uninitialized.sol --disable-solc-warnings --detectors uninitialized-state if [ $? -ne 1 ]; then exit 1 fi # contains also the test for the suicidal detector -slither tests/backdoor.sol --disable-solc-warnings --detect-backdoor +slither tests/backdoor.sol --disable-solc-warnings --detectors backdoor if [ $? -ne 1 ]; then exit 1 fi -slither tests/pragma.0.4.24.sol --disable-solc-warnings --detect-pragma +slither tests/pragma.0.4.24.sol --disable-solc-warnings --detectors pragma if [ $? -ne 1 ]; then exit 1 fi -slither tests/old_solc.sol.json --solc-ast --detect-solc-version +slither tests/old_solc.sol.json --solc-ast --detectors solc-version if [ $? -ne 1 ]; then exit 1 fi -slither tests/reentrancy.sol --disable-solc-warnings --detect-reentrancy +slither tests/reentrancy.sol --disable-solc-warnings --detectors reentrancy if [ $? -ne 1 ]; then exit 1 fi -slither tests/uninitialized_storage_pointer.sol --disable-solc-warnings --detect-uninitialized-storage +slither tests/uninitialized_storage_pointer.sol --disable-solc-warnings --detectors uninitialized-storage if [ $? -ne 1 ]; then exit 1 fi -slither tests/tx_origin.sol --disable-solc-warnings --detect-tx-origin +slither tests/tx_origin.sol --disable-solc-warnings --detectors tx-origin if [ $? -ne 2 ]; then exit 1 fi -slither tests/unused_state.sol --detect-unused-state +slither tests/unused_state.sol --detectors unused-state if [ $? -ne 1 ]; then exit 1 fi -slither tests/locked_ether.sol --detect-locked-ether +slither tests/locked_ether.sol --detectors locked-ether if [ $? -ne 1 ]; then exit 1 fi -slither tests/arbitrary_send.sol --disable-solc-warnings --detect-arbitrary-send +slither tests/arbitrary_send.sol --disable-solc-warnings --detectors arbitrary-send if [ $? -ne 2 ]; then exit 1 fi -slither tests/inline_assembly_contract.sol --disable-solc-warnings --detect-assembly +slither tests/inline_assembly_contract.sol --disable-solc-warnings --detectors assembly if [ $? -ne 1 ]; then exit 1 fi -slither tests/inline_assembly_library.sol --disable-solc-warnings --detect-assembly +slither tests/inline_assembly_library.sol --disable-solc-warnings --detectors assembly if [ $? -ne 2 ]; then exit 1 fi -slither tests/naming_convention.sol --disable-solc-warnings --detect-naming-convention +slither tests/naming_convention.sol --disable-solc-warnings --detectors naming-convention if [ $? -ne 10 ]; then exit 1 fi -slither tests/low_level_calls.sol --disable-solc-warnings --detect-low-level-calls +slither tests/low_level_calls.sol --disable-solc-warnings --detectors low-level-calls if [ $? -ne 1 ]; then exit 1 fi -slither tests/const_state_variables.sol --detect-const-candidates-state +slither tests/const_state_variables.sol --detectors const-candidates-state if [ $? -ne 2 ]; then exit 1 fi diff --git a/slither/__main__.py b/slither/__main__.py index 9c59f0205..72d83a331 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -8,7 +8,7 @@ import os import sys import traceback -from pkg_resources import iter_entry_points +from pkg_resources import iter_entry_points, require from slither.detectors.abstract_detector import (AbstractDetector, DetectorClassification, @@ -19,7 +19,8 @@ from slither.slither import Slither logging.basicConfig() logger = logging.getLogger("Slither") -def output_to_markdown(detector_classes): + +def output_detectors(detector_classes): """ Pretty print of the detectors to README.md """ @@ -27,8 +28,6 @@ def output_to_markdown(detector_classes): 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] @@ -43,7 +42,27 @@ def output_to_markdown(detector_classes): help_info, classification_txt[impact], confidence)) - idx = idx +1 + idx = idx + 1 + + +def output_printers(printer_classes): + """ + Pretty print of the printers to README.md + """ + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + + print(printers_list) + # Sort by name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + print('{} | `{}` | {} '.format(idx, argument, help_info)) + idx = idx + 1 + def process(filename, args, detector_classes, printer_classes): """ @@ -64,15 +83,13 @@ def process(filename, args, detector_classes, printer_classes): results = [] - if printer_classes: - slither.run_printers() # Currently printers does not return results + detector_results = slither.run_detectors() + detector_results = [x for x in detector_results if x] # remove empty results + detector_results = [item for sublist in detector_results for item in sublist] # flatten - elif detector_classes: - detector_results = slither.run_detectors() - detector_results = [x for x in detector_results if x] # remove empty results - detector_results = [item for sublist in detector_results for item in sublist] # flatten + results.extend(detector_results) - results.extend(detector_results) + slither.run_printers() # Currently printers does not return results return results, analyzed_contracts_count @@ -164,8 +181,12 @@ def main_impl(all_detector_classes, all_printer_classes): """ args = parse_args(all_detector_classes, all_printer_classes) - if args.markdown: - output_to_markdown(all_detector_classes) + if args.list_detectors: + output_detectors(all_detector_classes) + return + + if args.list_printers: + output_printers(all_printer_classes) return detector_classes = choose_detectors(args, all_detector_classes) @@ -205,9 +226,6 @@ def main_impl(all_detector_classes, all_printer_classes): (results_tmp, number_contracts_tmp) = process(filename, args, detector_classes, printer_classes) number_contracts += number_contracts_tmp results += results_tmp - # if args.json: - # output_json(results, args.json) - # exit(results) else: raise Exception("Unrecognised file/dir path: '#{filename}'".format(filename=filename)) @@ -229,17 +247,48 @@ def main_impl(all_detector_classes, all_printer_classes): def parse_args(detector_classes, printer_classes): parser = argparse.ArgumentParser(description='Slither', - usage="slither.py contract.sol [flag]", - formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=35)) + usage="slither.py contract.sol [flag]") parser.add_argument('filename', help='contract.sol file') + parser.add_argument('--version', + help='displays the current version', + version=require('slither-analyzer')[0].version, + action='version') + parser.add_argument('--solc', help='solc path', action='store', default='solc') + parser.add_argument('--detectors', + help='Comma-separated list of detectors, defaults to all, ' + 'available detectors: {}'.format(', '.join(d.ARGUMENT for d in detector_classes)), + action='store', + dest='detectors_to_run', + default='all') + + parser.add_argument('--printers', + help='Comma-separated list fo contract information printers, ' + 'defaults to contract-summary and can be disabled by using \'none\', ' + 'available printers: {}'.format(', '.join(d.ARGUMENT for d in printer_classes)), + action='store', + dest='printers_to_run', + default='contract-summary') + + parser.add_argument('--output', + help='Define output encoding', + action='store', + choices=['stdout', 'json'], + default='stdout') + + parser.add_argument('--exclude-detectors', + help='Comma-separated list of detectors that should be excluded', + action='store', + dest='detectors_to_exclude', + default='') + parser.add_argument('--solc-args', help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', action='store', @@ -280,34 +329,6 @@ def parse_args(detector_classes, printer_classes): action='store_true', default=False) - for detector_cls in detector_classes: - detector_arg = '--detect-{}'.format(detector_cls.ARGUMENT) - detector_help = '{}'.format(detector_cls.HELP) - parser.add_argument(detector_arg, - help=detector_help, - action="append_const", - dest="detectors_to_run", - const=detector_cls.ARGUMENT) - - # Second loop so that the --exclude are shown after all the detectors - for detector_cls in detector_classes: - exclude_detector_arg = '--exclude-{}'.format(detector_cls.ARGUMENT) - exclude_detector_help = 'Exclude {} detector'.format(detector_cls.ARGUMENT) - parser.add_argument(exclude_detector_arg, - help=exclude_detector_help, - action="append_const", - dest="detectors_to_exclude", - const=detector_cls.ARGUMENT) - - for printer_cls in printer_classes: - printer_arg = '--printer-{}'.format(printer_cls.ARGUMENT) - printer_help = 'Print {}'.format(printer_cls.HELP) - parser.add_argument(printer_arg, - help=printer_help, - action="append_const", - dest="printers_to_run", - const=printer_cls.ARGUMENT) - # debugger command parser.add_argument('--debug', help=argparse.SUPPRESS, @@ -319,15 +340,44 @@ def parse_args(detector_classes, printer_classes): action="store_true", default=False) - return parser.parse_args() + parser.add_argument('--list-detectors', + help='List available detectors', + action='store_true', + default=False) + + parser.add_argument('--list-printers', + help='List available printers', + action='store_true', + default=False) + + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + + args = parser.parse_args() + + return args def choose_detectors(args, all_detector_classes): # If detectors are specified, run only these ones - if args.detectors_to_run: - return [d for d in all_detector_classes if d.ARGUMENT in args.detectors_to_run] - detectors_to_run = all_detector_classes + detectors_to_run = [] + detectors = {d.ARGUMENT: d for d in all_detector_classes} + + if args.detectors_to_run == 'all': + detectors_to_run = all_detector_classes + detectors_excluded = args.detectors_to_exclude.split(',') + for d in detectors: + if d in detectors_excluded: + detectors_to_run.remove(detectors[d]) + else: + for d in args.detectors_to_run.split(','): + if d in detectors: + detectors_to_run.append(detectors[d]) + else: + raise Exception('Error: {} is not a detector'.format(d)) + return detectors_to_run if args.exclude_informational: detectors_to_run = [d for d in detectors_to_run if @@ -348,11 +398,18 @@ def choose_detectors(args, all_detector_classes): def choose_printers(args, all_printer_classes): - # by default, dont run any printer printers_to_run = [] - if args.printers_to_run: - printers_to_run = [p for p in all_printer_classes if - p.ARGUMENT in args.printers_to_run] + + # disable default printer + if args.printers_to_run == 'none': + return printers_to_run + + printers = {p.ARGUMENT: p for p in all_printer_classes} + for p in args.printers_to_run.split(','): + if p in printers: + printers_to_run.append(printers[p]) + else: + raise Exception('Error: {} is not a printer'.format(p)) return printers_to_run From d3aa4e069b9b10635a9377d9350863de5b72355c Mon Sep 17 00:00:00 2001 From: Josselin Date: Fri, 26 Oct 2018 10:28:50 +0100 Subject: [PATCH 2/2] - Group commands - Use PrettyTable for list-detectors/list-printers - Remove default printer - Remove --output --- slither/__main__.py | 257 ++++++++++++++++------------------ slither/utils/command_line.py | 67 +++++++++ 2 files changed, 187 insertions(+), 137 deletions(-) create mode 100644 slither/utils/command_line.py diff --git a/slither/__main__.py b/slither/__main__.py index 72d83a331..b2b075a35 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -11,59 +11,15 @@ import traceback from pkg_resources import iter_entry_points, require from slither.detectors.abstract_detector import (AbstractDetector, - DetectorClassification, - classification_txt) + DetectorClassification) from slither.printers.abstract_printer import AbstractPrinter from slither.slither import Slither +from slither.utils.command_line import output_to_markdown, output_detectors, output_printers logging.basicConfig() logger = logging.getLogger("Slither") -def output_detectors(detector_classes): - """ - Pretty print of the detectors to README.md - """ - detectors_list = [] - for detector in detector_classes: - argument = detector.ARGUMENT - # dont show the backdoor example - help_info = detector.HELP - 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('{} | `{}` | {} | {} | {}'.format(idx, - argument, - help_info, - classification_txt[impact], - confidence)) - idx = idx + 1 - - -def output_printers(printer_classes): - """ - Pretty print of the printers to README.md - """ - printers_list = [] - for printer in printer_classes: - argument = printer.ARGUMENT - help_info = printer.HELP - printers_list.append((argument, help_info)) - - print(printers_list) - # Sort by name - printers_list = sorted(printers_list, key=lambda element: (element[0])) - idx = 1 - for (argument, help_info) in printers_list: - print('{} | `{}` | {} '.format(idx, argument, help_info)) - idx = idx + 1 - - def process(filename, args, detector_classes, printer_classes): """ The core high-level code for running Slither static analysis. @@ -105,7 +61,7 @@ def exit(results): sys.exit(len(results)) -def main(): +def get_detectors_and_printers(): """ NOTE: This contains just a few detectors and printers that we made public. """ @@ -171,6 +127,11 @@ def main(): detectors += list(plugin_detectors) printers += list(plugin_printers) + return detectors, printers + +def main(): + detectors, printers = get_detectors_and_printers() + main_impl(all_detector_classes=detectors, all_printer_classes=printers) @@ -181,16 +142,11 @@ def main_impl(all_detector_classes, all_printer_classes): """ args = parse_args(all_detector_classes, all_printer_classes) - if args.list_detectors: - output_detectors(all_detector_classes) - return - - if args.list_printers: - output_printers(all_printer_classes) - return - - detector_classes = choose_detectors(args, all_detector_classes) printer_classes = choose_printers(args, all_printer_classes) + if printer_classes: + detector_classes = [] + else: + detector_classes = choose_detectors(args, all_detector_classes) default_log = logging.INFO if not args.debug else logging.DEBUG @@ -250,84 +206,100 @@ def parse_args(detector_classes, printer_classes): usage="slither.py contract.sol [flag]") parser.add_argument('filename', - help='contract.sol file') + help='contract.sol') parser.add_argument('--version', help='displays the current version', version=require('slither-analyzer')[0].version, action='version') - parser.add_argument('--solc', - help='solc path', - action='store', - default='solc') - - parser.add_argument('--detectors', - help='Comma-separated list of detectors, defaults to all, ' - 'available detectors: {}'.format(', '.join(d.ARGUMENT for d in detector_classes)), - action='store', - dest='detectors_to_run', - default='all') - - parser.add_argument('--printers', - help='Comma-separated list fo contract information printers, ' - 'defaults to contract-summary and can be disabled by using \'none\', ' - 'available printers: {}'.format(', '.join(d.ARGUMENT for d in printer_classes)), - action='store', - dest='printers_to_run', - default='contract-summary') - - parser.add_argument('--output', - help='Define output encoding', - action='store', - choices=['stdout', 'json'], - default='stdout') - - parser.add_argument('--exclude-detectors', - help='Comma-separated list of detectors that should be excluded', - action='store', - dest='detectors_to_exclude', - default='') - - parser.add_argument('--solc-args', - help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', - action='store', - default=None) - - parser.add_argument('--disable-solc-warnings', - help='Disable solc warnings', - action='store_true', - default=False) + group_detector = parser.add_argument_group('Detectors') + group_printer = parser.add_argument_group('Printers') + group_solc = parser.add_argument_group('Solc options') + group_misc = parser.add_argument_group('Additional option') + + group_detector.add_argument('--detectors', + help='Comma-separated list of detectors, defaults to all, ' + 'available detectors: {}'.format( + ', '.join(d.ARGUMENT for d in detector_classes)), + action='store', + dest='detectors_to_run', + default='all') + + group_printer.add_argument('--printers', + help='Comma-separated list fo contract information printers, ' + 'available printers: {}'.format( + ', '.join(d.ARGUMENT for d in printer_classes)), + action='store', + dest='printers_to_run', + default='') + + 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-detectors', + help='Comma-separated list of detectors that should be excluded', + action='store', + dest='detectors_to_exclude', + default='') + + group_detector.add_argument('--exclude-informational', + help='Exclude informational impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-low', + help='Exclude low impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-medium', + help='Exclude medium impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-high', + help='Exclude high impact analyses', + action='store_true', + default=False) + + + group_solc.add_argument('--solc', + help='solc path', + action='store', + default='solc') + + group_solc.add_argument('--solc-args', + help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', + action='store', + default=None) + + group_solc.add_argument('--disable-solc-warnings', + help='Disable solc warnings', + action='store_true', + default=False) + + group_solc.add_argument('--solc-ast', + help='Provide the ast solc file', + action='store_true', + default=False) + + group_misc.add_argument('--json', + help='Export results as JSON', + action='store', + default=None) - parser.add_argument('--solc-ast', - help='Provide the ast solc file', - action='store_true', - default=False) - parser.add_argument('--json', - help='Export results as JSON', - action='store', - default=None) - - parser.add_argument('--exclude-informational', - help='Exclude informational impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-low', - help='Exclude low impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-medium', - help='Exclude medium impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-high', - help='Exclude high impact analyses', - action='store_true', - default=False) # debugger command parser.add_argument('--debug', @@ -337,18 +309,10 @@ def parse_args(detector_classes, printer_classes): parser.add_argument('--markdown', help=argparse.SUPPRESS, - action="store_true", + action=OutputMarkdown, default=False) - parser.add_argument('--list-detectors', - help='List available detectors', - action='store_true', - default=False) - parser.add_argument('--list-printers', - help='List available printers', - action='store_true', - default=False) if len(sys.argv) == 1: parser.print_help(sys.stderr) @@ -358,6 +322,25 @@ def parse_args(detector_classes, printer_classes): return args +class ListDetectors(argparse.Action): + def __call__(self, parser, *args, **kwargs): + detectors, _ = get_detectors_and_printers() + output_detectors(detectors) + parser.exit() + +class ListPrinters(argparse.Action): + def __call__(self, parser, *args, **kwargs): + _, printers = get_detectors_and_printers() + output_printers(printers) + parser.exit() + +class OutputMarkdown(argparse.Action): + def __call__(self, parser, *args, **kwargs): + detectors, _ = get_detectors_and_printers() + output_to_markdown(detectors) + parser.exit() + + def choose_detectors(args, all_detector_classes): # If detectors are specified, run only these ones @@ -401,8 +384,8 @@ def choose_printers(args, all_printer_classes): printers_to_run = [] # disable default printer - if args.printers_to_run == 'none': - return printers_to_run + if args.printers_to_run == '': + return [] printers = {p.ARGUMENT: p for p in all_printer_classes} for p in args.printers_to_run.split(','): diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py new file mode 100644 index 000000000..90a933fc3 --- /dev/null +++ b/slither/utils/command_line.py @@ -0,0 +1,67 @@ +from prettytable import PrettyTable + +from slither.detectors.abstract_detector import classification_txt + +def output_to_markdown(detector_classes): + 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)) + + # 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('{} | `{}` | {} | {} | {}'.format(idx, + argument, + help_info, + classification_txt[impact], + confidence)) + idx = idx + 1 + + +def output_detectors(detector_classes): + detectors_list = [] + for detector in detector_classes: + argument = detector.ARGUMENT + help_info = detector.HELP + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + detectors_list.append((argument, help_info, impact, confidence)) + table = PrettyTable(["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([idx, argument, help_info, classification_txt[impact], confidence]) + idx = idx + 1 + print(table) + +def output_printers(printer_classes): + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + table = PrettyTable(["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([idx, argument, help_info]) + idx = idx + 1 + print(table)