From f5a46b8b0af26f9cac544ac12580af49f35f9e22 Mon Sep 17 00:00:00 2001 From: Matej Voboril Date: Sat, 20 Jul 2024 19:52:30 -0500 Subject: [PATCH 1/3] feat: add windows-specific installers for some tools --- fsociety/__main__.py | 52 +++++++++++++++++++--- fsociety/core/config.py | 2 + fsociety/core/errors.py | 28 ++++++++++++ fsociety/core/menu.py | 37 ++++++++------- fsociety/core/repo.py | 41 ++++++++++++++++- fsociety/information_gathering/__init__.py | 2 +- fsociety/networking/__init__.py | 2 +- fsociety/networking/bettercap.py | 2 +- fsociety/networking/nmap.py | 4 +- fsociety/obfuscation/__init__.py | 2 +- fsociety/passwords/__init__.py | 2 +- fsociety/web_apps/__init__.py | 2 +- mypy.ini | 2 +- 13 files changed, 144 insertions(+), 34 deletions(-) create mode 100644 fsociety/core/errors.py diff --git a/fsociety/__main__.py b/fsociety/__main__.py index 077b7a0..653d97d 100644 --- a/fsociety/__main__.py +++ b/fsociety/__main__.py @@ -3,8 +3,10 @@ import platform import sys from random import choice +from typing import Optional, Union from rich.columns import Columns +from rich.style import Style from rich.text import Text import fsociety.core.utilities @@ -20,6 +22,7 @@ format_tools, module_name, prompt, + run_tool, set_readline, ) @@ -112,20 +115,59 @@ def agreement(): config.set("fsociety", "agreement", "true") +subcommands = list() for item in MENU_ITEMS: items[module_name(item)] = item - + for tool in item.__tools__: + subcommands.append( + { + "parent": item.__name__.split(".")[-1], + "tool": tool, + "name": tool.__str__(), + } + ) commands = list(items.keys()) + list(BUILTIN_FUNCTIONS.keys()) +def errorhandle(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: + console.print(str(error), style=style) + if config.getboolean("fsociety", "development"): + console.print_exception() + input("Press [Enter/Return] to return to menu... ") + return + + +def doexcept(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: + try: + raise error + except Exception as e: + errorhandle(e, style=style) + + def mainloop(): agreement() console.print(choice(BANNERS), style="red", highlight=False) print_menu_items() selected_command = input(prompt()).strip() + + def sub_command_has(sbc): + return sbc["name"] == selected_command + if not selected_command or (selected_command not in commands): - console.print("Invalid Command", style="bold yellow") - return + try: + matches = list(filter(sub_command_has, subcommands)) + subcommand = None + if len(matches) > 0: + subcommand = matches[0] + if subcommand is not None: + # execute parent cli, pass subcmd.name -> cli + return run_tool(subcommand["tool"], subcommand["name"]) + doexcept(Exception("Invalid Command"), style="bold yellow") + except StopIteration: + # looks like we didn't find a subcommand, either + return errorhandle(Exception("Invalid Command"), style="bold yellow") + except Exception as error: + return errorhandle(error) if selected_command in BUILTIN_FUNCTIONS: func = BUILTIN_FUNCTIONS.get(selected_command) return func() @@ -133,9 +175,7 @@ def mainloop(): func = items[selected_command].cli return func() except Exception as error: - console.print(str(error)) - console.print_exception() - return + return errorhandle(error) def info(): diff --git a/fsociety/core/config.py b/fsociety/core/config.py index dec42b6..f9a4ea0 100644 --- a/fsociety/core/config.py +++ b/fsociety/core/config.py @@ -15,6 +15,7 @@ elif platform.startswith("linux") or platform.startswith("freebsd"): CURRENT_PLATFORM = distro.like() + INSTALL_DIR = os.path.join(str(Path.home()), ".fsociety") CONFIG_FILE = os.path.join(INSTALL_DIR, "fsociety.cfg") GITHUB_PATH = "fsociety-team/fsociety" @@ -26,6 +27,7 @@ "os": CURRENT_PLATFORM, "host_file": "hosts.txt", "usernames_file": "usernames.txt", + "log_level": "error", } diff --git a/fsociety/core/errors.py b/fsociety/core/errors.py new file mode 100644 index 0000000..39f2abe --- /dev/null +++ b/fsociety/core/errors.py @@ -0,0 +1,28 @@ +from typing import Optional, Union + +from rich.style import Style + +from fsociety.console import console +from fsociety.core.config import get_config + +# Config +config = get_config() + + +def input_wait(): + input("\nPress [ENTER] to continue... ") + + +def errorhandle(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: + console.print(str(error), style=style) + if config.get("fsociety", "log_level") == "debug": + console.print_exception() + input_wait() + return + + +def doexcept(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: + try: + raise error + except Exception as e: + errorhandle(e, style=style) diff --git a/fsociety/core/menu.py b/fsociety/core/menu.py index 2743d7d..5a951a7 100644 --- a/fsociety/core/menu.py +++ b/fsociety/core/menu.py @@ -1,5 +1,4 @@ import os -import shutil from typing import Iterable from rich import box @@ -9,6 +8,7 @@ from fsociety.console import console from fsociety.core.config import INSTALL_DIR +from fsociety.core.errors import doexcept BACK_COMMANDS = ["back", "return"] @@ -74,8 +74,22 @@ def prompt(path="", base_path="~"): return f"\nfsociety {encoded_path}# " -def input_wait(): - input("\nPress [ENTER] to continue... ") +def run_tool(tool, selected_tool: str): + if hasattr(tool, "install") and not tool.installed(): + tool.install() + try: + response = tool.run() + if response and response > 0 and response != 256: + console.print( + f"{selected_tool} returned a non-zero exit code", style="bold red" + ) + if hasattr(tool, "install") and confirm("Do you want to reinstall?"): + os.chdir(INSTALL_DIR) + tool.uninstall() + tool.install() + doexcept(Exception(f"{selected_tool} completed"), style="bold green on green") + except KeyboardInterrupt: + return def tools_cli(name, tools, links=True): @@ -107,22 +121,7 @@ def tools_cli(name, tools, links=True): console.print("Invalid Command", style="bold yellow") return tools_cli(name, tools, links) tool = tools_dict.get(selected_tool) - if hasattr(tool, "install") and not tool.installed(): - tool.install() - try: - response = tool.run() - if response and response > 0 and response != 256: - console.print( - f"{selected_tool} returned a non-zero exit code", style="bold red" - ) - if hasattr(tool, "install") and confirm("Do you want to reinstall?"): - os.chdir(INSTALL_DIR) - shutil.rmtree(tool.full_path) - tool.install() - except KeyboardInterrupt: - return - - return input_wait() + return run_tool(tool, selected_tool) def confirm(message="Do you want to?"): diff --git a/fsociety/core/repo.py b/fsociety/core/repo.py index 2fb9f3b..3ff0af1 100644 --- a/fsociety/core/repo.py +++ b/fsociety/core/repo.py @@ -122,6 +122,39 @@ def clone(self, overwrite: bool = False) -> str: raise CloneError(f"{self.full_path} not found") return self.full_path + def uninstallable(self) -> bool: + if self.install_options: + install = self.install_options + target_os = config.get("fsociety", "os") + if isinstance(install, dict): + if target_os == "macos" and "brew" in install and which("brew"): + return True + elif target_os == "windows" and "winget" in install and which("winget"): + return True + elif target_os == "windows" and "scoop" in install and which("scoop"): + return True + return False + + def uninstall(self) -> None: + if self.install_options: + install = self.install_options + target_os = config.get("fsociety", "os") + command = "exit 1" + if isinstance(install, dict): + if "brew" in install and which("brew"): + brew_opts = install.get("brew") + command = f"brew uninstall {brew_opts}" + elif target_os == "windows" and "winget" in install and which("winget"): + winget_opts = install.get("winget") + command = f"winget uninstall {winget_opts}" + elif target_os == "windows" and "scoop" in install and which("scoop"): + scoop_opts = install.get("scoop") + command = f"scoop uninstall {scoop_opts}" + if command == "exit 1": + rmtree(self.full_path) + else: + os.system(command) + def install(self, no_confirm: bool = False, clone: bool = True) -> None: if no_confirm or not confirm( f"\nDo you want to install https://github.com/{self.path}?" @@ -175,7 +208,13 @@ def install(self, no_confirm: bool = False, clone: bool = True) -> None: command = f"mkdir {self.full_path} && {command} && chmod +x {self.full_path}/{self.name}" elif "brew" in install and which("brew"): brew_opts = install.get("brew") - command = f"brew {brew_opts}" + command = f"brew install {brew_opts}" + elif target_os == "windows" and "winget" in install and which("winget"): + winget_opts = install.get("winget") + command = f"winget install {winget_opts}" + elif target_os == "windows" and "scoop" in install and which("scoop"): + scoop_opts = install.get("scoop") + command = f"scoop install {scoop_opts}" elif target_os in install and target_os in self.scriptable_os: command = str(install[target_os]) else: diff --git a/fsociety/information_gathering/__init__.py b/fsociety/information_gathering/__init__.py index 046326e..0de7d04 100644 --- a/fsociety/information_gathering/__init__.py +++ b/fsociety/information_gathering/__init__.py @@ -1,3 +1,3 @@ from .cli import __tools__, cli -__all__ = ["cli", "__tools__"] + [str(tool) for tool in __tools__] +__all__ = ["cli", "__tools__", "__name__"] + [str(tool) for tool in __tools__] diff --git a/fsociety/networking/__init__.py b/fsociety/networking/__init__.py index 046326e..0de7d04 100644 --- a/fsociety/networking/__init__.py +++ b/fsociety/networking/__init__.py @@ -1,3 +1,3 @@ from .cli import __tools__, cli -__all__ = ["cli", "__tools__"] + [str(tool) for tool in __tools__] +__all__ = ["cli", "__tools__", "__name__"] + [str(tool) for tool in __tools__] diff --git a/fsociety/networking/bettercap.py b/fsociety/networking/bettercap.py index e5daff4..e96940e 100644 --- a/fsociety/networking/bettercap.py +++ b/fsociety/networking/bettercap.py @@ -11,7 +11,7 @@ def __init__(self): install={ "linux": "sudo apt install golang git build-essential libpcap-dev libusb-1.0-0-dev libnetfilter-queue-dev; go get -u github.com/bettercap/bettercap", "arch": "sudo pacman -Sy bettercap", - "brew": "install bettercap", + "brew": "bettercap", }, description="Swiss army knife for network attacks and monitoring", ) diff --git a/fsociety/networking/nmap.py b/fsociety/networking/nmap.py index 109826d..f90319c 100644 --- a/fsociety/networking/nmap.py +++ b/fsociety/networking/nmap.py @@ -29,8 +29,10 @@ def __init__(self): path="nmap/nmap", install={ "arch": "sudo pacman -Sy nmap", - "brew": "install nmap", + "brew": "nmap", "linux": "sudo apt-get install nmap", + "scoop": "nmap extras/vcredist2008", + "winget": "nmap", }, description="the Network Mapper", ) diff --git a/fsociety/obfuscation/__init__.py b/fsociety/obfuscation/__init__.py index 046326e..0de7d04 100644 --- a/fsociety/obfuscation/__init__.py +++ b/fsociety/obfuscation/__init__.py @@ -1,3 +1,3 @@ from .cli import __tools__, cli -__all__ = ["cli", "__tools__"] + [str(tool) for tool in __tools__] +__all__ = ["cli", "__tools__", "__name__"] + [str(tool) for tool in __tools__] diff --git a/fsociety/passwords/__init__.py b/fsociety/passwords/__init__.py index 046326e..0de7d04 100644 --- a/fsociety/passwords/__init__.py +++ b/fsociety/passwords/__init__.py @@ -1,3 +1,3 @@ from .cli import __tools__, cli -__all__ = ["cli", "__tools__"] + [str(tool) for tool in __tools__] +__all__ = ["cli", "__tools__", "__name__"] + [str(tool) for tool in __tools__] diff --git a/fsociety/web_apps/__init__.py b/fsociety/web_apps/__init__.py index 046326e..0de7d04 100644 --- a/fsociety/web_apps/__init__.py +++ b/fsociety/web_apps/__init__.py @@ -1,3 +1,3 @@ from .cli import __tools__, cli -__all__ = ["cli", "__tools__"] + [str(tool) for tool in __tools__] +__all__ = ["cli", "__tools__", "__name__"] + [str(tool) for tool in __tools__] diff --git a/mypy.ini b/mypy.ini index 6def55d..578a3e6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.6 +python_version = 3.8 files = fsociety [mypy-git.*] From ec084964e5db6d41edb80562a00c2c8810de99b6 Mon Sep 17 00:00:00 2001 From: Matej Voboril Date: Sun, 21 Jul 2024 13:09:44 -0500 Subject: [PATCH 2/3] feat: make a verbose console interface --- fsociety/__main__.py | 29 ++++------------ fsociety/console.py | 77 +++++++++++++++++++++++++++++++++++++++-- fsociety/core/errors.py | 28 --------------- fsociety/core/menu.py | 11 +++--- 4 files changed, 85 insertions(+), 60 deletions(-) delete mode 100644 fsociety/core/errors.py diff --git a/fsociety/__main__.py b/fsociety/__main__.py index 653d97d..d97f0af 100644 --- a/fsociety/__main__.py +++ b/fsociety/__main__.py @@ -3,10 +3,8 @@ import platform import sys from random import choice -from typing import Optional, Union from rich.columns import Columns -from rich.style import Style from rich.text import Text import fsociety.core.utilities @@ -103,13 +101,13 @@ def print_menu_items(): for key in BUILTIN_FUNCTIONS: print() - console.print(key, style="command") + console.command(key) def agreement(): while not config.getboolean("fsociety", "agreement"): clear_screen() - console.print(TERMS, style="bold yellow") + console.warning(TERMS) agree = input("You must agree to our terms and conditions first (Y/n) ") if agree.lower()[0] == "y": config.set("fsociety", "agreement", "true") @@ -129,21 +127,6 @@ def agreement(): commands = list(items.keys()) + list(BUILTIN_FUNCTIONS.keys()) -def errorhandle(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: - console.print(str(error), style=style) - if config.getboolean("fsociety", "development"): - console.print_exception() - input("Press [Enter/Return] to return to menu... ") - return - - -def doexcept(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: - try: - raise error - except Exception as e: - errorhandle(e, style=style) - - def mainloop(): agreement() console.print(choice(BANNERS), style="red", highlight=False) @@ -162,12 +145,12 @@ def sub_command_has(sbc): if subcommand is not None: # execute parent cli, pass subcmd.name -> cli return run_tool(subcommand["tool"], subcommand["name"]) - doexcept(Exception("Invalid Command"), style="bold yellow") + return console.warning("Invalid Command") except StopIteration: # looks like we didn't find a subcommand, either - return errorhandle(Exception("Invalid Command"), style="bold yellow") + return console.warning("Invalid Command") except Exception as error: - return errorhandle(error) + return console.handle_error(error) if selected_command in BUILTIN_FUNCTIONS: func = BUILTIN_FUNCTIONS.get(selected_command) return func() @@ -175,7 +158,7 @@ def sub_command_has(sbc): func = items[selected_command].cli return func() except Exception as error: - return errorhandle(error) + return console.handle_error(error) def info(): diff --git a/fsociety/console.py b/fsociety/console.py index 8f69b16..916c9c0 100644 --- a/fsociety/console.py +++ b/fsociety/console.py @@ -1,7 +1,13 @@ -from rich.console import Console +from enum import Enum + +from rich.console import Console as RichConsole from rich.theme import Theme from rich.traceback import install +from fsociety.core.config import get_config + +config = get_config() + # Install Traceback install() @@ -10,6 +16,73 @@ { "command": "black on white", "warning": "bold yellow", + "error": "bold red", + "info": "bold blue", + "debug": "bright_black", + "success": "bold green on green", } ) -console = Console(theme=fsociety_theme) + + +class ConsoleLogLevel(Enum): + DEBUG = "debug" + INFO = "info" + ERROR = "error" + WARNING = "warning" + + +def input_wait(): + input("\nPress [ENTER] to continue... ") + + +class Console(RichConsole): + """Console class for fsociety""" + + log_level: ConsoleLogLevel = ConsoleLogLevel.ERROR + + def __init__(self, log_level: ConsoleLogLevel = ConsoleLogLevel.ERROR): + super().__init__(theme=fsociety_theme) + + def set_log_level(self, level: ConsoleLogLevel): + self.log_level = level + + def debug(self, message: str, error: Exception = None): + if self.log_level == ConsoleLogLevel.DEBUG: + self.print(message, style="debug") + if error: + self.print_exception() + + def info(self, message: str): + if self.log_level in [ConsoleLogLevel.DEBUG, ConsoleLogLevel.INFO]: + self.print(message, style="info") + + def warning(self, message: str): + if self.log_level in [ + ConsoleLogLevel.DEBUG, + ConsoleLogLevel.INFO, + ConsoleLogLevel.WARNING, + ]: + self.print(message, style="warning") + + def error(self, message: str): + self.print(message, style="error") + if self.log_level == ConsoleLogLevel.DEBUG: + self.print_exception() + + def handle_error(self, error: Exception): + self.error(str(error)) + if self.log_level == ConsoleLogLevel.DEBUG: + self.print_exception() + input_wait() + + def success(self, message: str): + self.print(message, style="success") + input_wait() + + def command(self, message: str): + self.print(message, style="command") + + +default_log_level = config.get("fsociety", "log_level") + +console = Console(default_log_level) diff --git a/fsociety/core/errors.py b/fsociety/core/errors.py deleted file mode 100644 index 39f2abe..0000000 --- a/fsociety/core/errors.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Optional, Union - -from rich.style import Style - -from fsociety.console import console -from fsociety.core.config import get_config - -# Config -config = get_config() - - -def input_wait(): - input("\nPress [ENTER] to continue... ") - - -def errorhandle(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: - console.print(str(error), style=style) - if config.get("fsociety", "log_level") == "debug": - console.print_exception() - input_wait() - return - - -def doexcept(error: Exception, style: Optional[Union[str, Style]] = "red") -> None: - try: - raise error - except Exception as e: - errorhandle(e, style=style) diff --git a/fsociety/core/menu.py b/fsociety/core/menu.py index 5a951a7..28b5ab6 100644 --- a/fsociety/core/menu.py +++ b/fsociety/core/menu.py @@ -8,7 +8,6 @@ from fsociety.console import console from fsociety.core.config import INSTALL_DIR -from fsociety.core.errors import doexcept BACK_COMMANDS = ["back", "return"] @@ -80,14 +79,12 @@ def run_tool(tool, selected_tool: str): try: response = tool.run() if response and response > 0 and response != 256: - console.print( - f"{selected_tool} returned a non-zero exit code", style="bold red" - ) + console.error(f"{selected_tool} returned a non-zero exit code") if hasattr(tool, "install") and confirm("Do you want to reinstall?"): os.chdir(INSTALL_DIR) tool.uninstall() tool.install() - doexcept(Exception(f"{selected_tool} completed"), style="bold green on green") + console.success(f"{selected_tool} completed") except KeyboardInterrupt: return @@ -110,7 +107,7 @@ def tools_cli(name, tools, links=True): table.add_row(*args) console.print(table) - console.print("back", style="command") + console.command("back") set_readline(list(tools_dict.keys()) + BACK_COMMANDS) selected_tool = input(prompt(name.split(".")[-2])).strip() if selected_tool not in tools_dict: @@ -118,7 +115,7 @@ def tools_cli(name, tools, links=True): return if selected_tool == "exit": raise KeyboardInterrupt - console.print("Invalid Command", style="bold yellow") + console.warn("Invalid Command") return tools_cli(name, tools, links) tool = tools_dict.get(selected_tool) return run_tool(tool, selected_tool) From 475bd7b05ccf09e0f558a5967998e874bd055e16 Mon Sep 17 00:00:00 2001 From: Matej Voboril Date: Sun, 21 Jul 2024 18:53:07 -0500 Subject: [PATCH 3/3] fix: typos, add optional wait_input --- fsociety/__main__.py | 15 +++++++-------- fsociety/console.py | 30 +++++++++++++++++------------- fsociety/core/config.py | 2 +- fsociety/core/menu.py | 8 +++----- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/fsociety/__main__.py b/fsociety/__main__.py index d97f0af..fb70e47 100644 --- a/fsociety/__main__.py +++ b/fsociety/__main__.py @@ -100,16 +100,15 @@ def print_menu_items(): console.print(Columns(cols, equal=True, expand=True)) for key in BUILTIN_FUNCTIONS: - print() - console.command(key) + console.command(f"\n{key}") def agreement(): while not config.getboolean("fsociety", "agreement"): clear_screen() - console.warning(TERMS) - agree = input("You must agree to our terms and conditions first (Y/n) ") - if agree.lower()[0] == "y": + console.input_error(TERMS) + agree = input("You must agree to our terms and conditions first (y/N) ") + if len(agree) and agree[0].lower() == "y": config.set("fsociety", "agreement", "true") @@ -145,10 +144,10 @@ def sub_command_has(sbc): if subcommand is not None: # execute parent cli, pass subcmd.name -> cli return run_tool(subcommand["tool"], subcommand["name"]) - return console.warning("Invalid Command") + return console.input_error("Invalid Command", True) except StopIteration: # looks like we didn't find a subcommand, either - return console.warning("Invalid Command") + return console.input_error("Invalid Command", True) except Exception as error: return console.handle_error(error) if selected_command in BUILTIN_FUNCTIONS: @@ -185,7 +184,7 @@ def interactive(): set_readline(commands) mainloop() except KeyboardInterrupt: - console.print("\nExitting...") + console.print("\nExiting...") write_config(config) sys.exit(0) diff --git a/fsociety/console.py b/fsociety/console.py index 916c9c0..b4daafe 100644 --- a/fsociety/console.py +++ b/fsociety/console.py @@ -42,32 +42,36 @@ class Console(RichConsole): def __init__(self, log_level: ConsoleLogLevel = ConsoleLogLevel.ERROR): super().__init__(theme=fsociety_theme) + self.log_level = ConsoleLogLevel(log_level) - def set_log_level(self, level: ConsoleLogLevel): - self.log_level = level + def set_log_level(self, level: str): + self.log_level = ConsoleLogLevel(level) - def debug(self, message: str, error: Exception = None): + def debug(self, message: str, error: Exception = None, wait_input: bool = False): if self.log_level == ConsoleLogLevel.DEBUG: self.print(message, style="debug") if error: self.print_exception() + if wait_input: + input_wait() - def info(self, message: str): + def info(self, message: str, wait_input: bool = False): if self.log_level in [ConsoleLogLevel.DEBUG, ConsoleLogLevel.INFO]: self.print(message, style="info") + if wait_input: + input_wait() - def warning(self, message: str): - if self.log_level in [ - ConsoleLogLevel.DEBUG, - ConsoleLogLevel.INFO, - ConsoleLogLevel.WARNING, - ]: - self.print(message, style="warning") + def input_error(self, message: str, wait_input: bool = False): + self.print(message, style="warning") + if wait_input: + input_wait() - def error(self, message: str): + def error(self, message: str, wait_input: bool = False): self.print(message, style="error") if self.log_level == ConsoleLogLevel.DEBUG: self.print_exception() + if wait_input: + input_wait() def handle_error(self, error: Exception): self.error(str(error)) @@ -85,4 +89,4 @@ def command(self, message: str): default_log_level = config.get("fsociety", "log_level") -console = Console(default_log_level) +console = Console(ConsoleLogLevel(default_log_level)) diff --git a/fsociety/core/config.py b/fsociety/core/config.py index f9a4ea0..dda9dec 100644 --- a/fsociety/core/config.py +++ b/fsociety/core/config.py @@ -27,7 +27,7 @@ "os": CURRENT_PLATFORM, "host_file": "hosts.txt", "usernames_file": "usernames.txt", - "log_level": "error", + "log_level": "info", } diff --git a/fsociety/core/menu.py b/fsociety/core/menu.py index 28b5ab6..f5bcaf5 100644 --- a/fsociety/core/menu.py +++ b/fsociety/core/menu.py @@ -115,14 +115,12 @@ def tools_cli(name, tools, links=True): return if selected_tool == "exit": raise KeyboardInterrupt - console.warn("Invalid Command") + console.warn("Invalid Command", True) return tools_cli(name, tools, links) tool = tools_dict.get(selected_tool) return run_tool(tool, selected_tool) def confirm(message="Do you want to?"): - response = input(f"{message} (y/n): ").lower() - if response: - return response[0] == "y" - return False + agree = input(f"{message} (y/N): ") + return len(agree) and agree[0].lower() == "y"