From 6517048b020f4afa967e0c45f7ecacc227ca6be4 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 09:31:30 -0400 Subject: [PATCH 01/47] Add new ui and new installation --- ghost/core/console.py | 131 +++++++++++++++++++++++++++++++++++++++ install.py | 140 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ setup.py | 89 ++++++++++++--------------- uninstall.py | 86 ++++++++++++++++++++++++++ 5 files changed, 402 insertions(+), 49 deletions(-) create mode 100755 install.py create mode 100644 requirements.txt create mode 100644 uninstall.py diff --git a/ghost/core/console.py b/ghost/core/console.py index 1abded23..3e62ece1 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -26,6 +26,27 @@ from ghost.core.device import Device +# --- Rich UI imports for styling (visual-only; logic untouched) --- +from rich.console import Console as RichConsole +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align +from rich import box +from rich.style import Style +from rich.rule import Rule +from rich.padding import Padding +from rich.columns import Columns +from rich.markdown import Markdown + +# Theme colors +PURPLE = "#7B61FF" +WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) +INFO_STYLE = Style(color=PURPLE, bold=True) +WARN_STYLE = Style(color="yellow", bold=True) +ERR_STYLE = Style(color="red", bold=True) +SUCCESS_STYLE = Style(color="green", bold=True) + class Console(Cmd): """ Subclass of ghost.core module. @@ -35,6 +56,7 @@ class Console(Cmd): """ def __init__(self) -> None: + # keep original prompt/intro tokens so framework behavior is unchanged super().__init__( prompt='(%lineghost%end)> ', intro="""%clear%end @@ -49,8 +71,117 @@ def __init__(self) -> None: """ ) + # preserve devices dict & logic self.devices = {} + # create rich console for all visual outputs + self.rich = RichConsole() + self._render_header() + + # ------------------------- + # Visual helpers (only) + # ------------------------- + def _render_header(self) -> None: + """Render a fancy hacker-style header with tools table.""" + title = Text("Ghost Framework 8.0.0", style="bold white") + subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") + + # ASCII art panel (kept similar to original) + ascii_art = Text( + " .--. .-. .-.\n" + " : .--': : .' `.\n" + " : : _ : `-. .--. .--.`. .'\n" + " : :; :: .. :' .; :`._-.': :\n" + " `.__.':_;:_;`.__.'`.__.':_;", + justify="center", + ) + + left = Panel( + Align.center(ascii_art), + border_style=PURPLE, + box=box.HEAVY, + padding=(0, 2), + title="[bold]" + "GHOST", + subtitle=title, + ) + + # Tools / Commands table (visual only) + tools_table = Table.grid(expand=True) + tools_table.add_column(ratio=1) + tools_table.add_column(ratio=2) + + tools = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + tools.add_column("Command", style="bold white", no_wrap=True) + tools.add_column("Description", style="dim") + + tools.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") + tools.add_row("devices", "List connected devices") + tools.add_row("disconnect ", "Disconnect device by ID") + tools.add_row("interact ", "Interact with a connected device") + tools.add_row("exit", "Quit Ghost Framework") + tools.add_row("Index 99", "Return to Menu / Exit (UI helper)") + + right_panel = Panel( + Align.left( + Text.assemble(subtitle, "\n\n", "Theme: ", (PURPLE, "Hacker • Purple")) + ), + border_style=PURPLE, + box=box.ROUNDED, + padding=(0, 1), + title="[bold]Info", + ) + + # combine header columns + header_columns = Columns([left, Panel(Padding(tools, (1, 2)), border_style=PURPLE), right_panel]) + self.rich.print(header_columns) + self.rich.print(Rule(style=PURPLE)) + self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) + self.rich.print() + + def print_empty(self, message: str = "") -> None: + # preserve signature and behavior, but print a small spacer + self.rich.print("") # purely visual + + def print_information(self, message: str) -> None: + # visually enhanced info box (preserves semantics) + self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) + + def print_warning(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) + + def print_error(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) + + def print_success(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) + + def print_usage(self, usage: str) -> None: + # show usage in an emphasized box + usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) + + def print_process(self, message: str) -> None: + # lightweight status indicator (visual only) + with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): + # no blocking logic here — just visual affordance + pass + + def print_table(self, title: str, columns: tuple, *rows) -> None: + """Render a stylized table for lists like connected devices.""" + table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + for col in columns: + table.add_column(str(col), header_style="bold white") + for row in rows: + # ensure each item is string (matching original behavior) + table.add_row(*[str(x) for x in row]) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) + self.rich.print(wrapper) + + # ------------------------- + # Original command logic (UNCHANGED) + # ------------------------- def do_exit(self, _) -> None: """ Exit Ghost Framework. diff --git a/install.py b/install.py new file mode 100755 index 00000000..2ffb708b --- /dev/null +++ b/install.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +install.py — Ghost installer (full) + +- Interactive choice: virtualenv / --brek / system +- Automatically installs Ghost itself +- Installs all GitHub dependencies even in --brek/system mode +- Adds --break-system-packages for system Python +- Uses rich for terminal output +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path +from typing import List, Tuple + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt +from rich.text import Text + +console = Console() +ROOT = Path(__file__).parent.resolve() + +# GitHub/URL dependencies +VCS_PACKAGES = [ + "badges @ git+https://github.com/EntySec/Badges", + "pex @ git+https://github.com/EntySec/Pex", + "colorscript @ git+https://github.com/EntySec/ColorScript", + "adb-shell" +] + +PY_PACKAGES: List[str] = [] # any pure python packages from requirements.txt + +DEFAULT_VENV = ROOT / ".venv" + + +def run(cmd: List[str], env: dict | None = None, check: bool = True) -> None: + console.log(f"[bold purple]$[/bold purple] {' '.join(cmd)}") + res = subprocess.run(cmd, env=env) + if check and res.returncode != 0: + raise SystemExit(f"Command failed: {' '.join(cmd)} (exit {res.returncode})") + + +def ensure_virtualenv(venv_path: Path) -> Tuple[str, str]: + if venv_path.exists(): + console.print(Panel(f"Using existing virtualenv: [bold]{venv_path}[/bold]", title="Virtualenv")) + else: + console.print(Panel(f"Creating virtualenv at: [bold]{venv_path}[/bold]", title="Virtualenv")) + run([sys.executable, "-m", "venv", str(venv_path)]) + if os.name == "nt": + py = venv_path / "Scripts" / "python.exe" + pip = venv_path / "Scripts" / "pip.exe" + else: + py = venv_path / "bin" / "python" + pip = venv_path / "bin" / "pip" + return str(py), str(pip) + + +def install_packages(py_exe: str, packages: List[str], break_system: bool = False) -> None: + if not packages: + return + cmd = [py_exe, "-m", "pip", "install"] + packages + if break_system: + cmd.append("--break-system-packages") + run(cmd) + + +def install_local_package(py_exe: str, break_system: bool = False) -> None: + cmd = [py_exe, "-m", "pip", "install", ".", "--no-deps"] + if break_system: + cmd.append("--break-system-packages") + run(cmd) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Ghost installer (venv or --brek/system)") + parser.add_argument("--no-venv", action="store_true", help="Do not create/use virtualenv; install in current interpreter") + parser.add_argument("--venv", default=".venv", help="Virtualenv path (default: .venv)") + parser.add_argument("--brek", action="store_true", help="Install only python-tool packages (filter out VCS/URLs).") + parser.add_argument("--yes", "-y", action="store_true", help="Automatic yes for prompts") + args = parser.parse_args() + + console.print(Panel(Text("Ghost Installer — staged installation\n\nIndex 99 → Return to Menu / Exit", justify="center"), style="purple")) + + use_brek = args.brek + use_venv = not args.no_venv + + if not (args.brek or args.no_venv): + choice = Prompt.ask( + "Choose install mode", + choices=["venv", "brek", "system"], + default="venv", + show_choices=True, + ) + if choice == "venv": + use_venv = True + use_brek = False + elif choice == "brek": + use_venv = False + use_brek = True + else: + use_venv = False + use_brek = False + + break_system_flag = False + if not use_venv: + break_system_flag = True + console.print(Panel("Installing into current Python interpreter (no virtualenv). --break-system-packages enabled", title="Notice", style="yellow")) + + if use_venv: + py_exe, pip_exe = ensure_virtualenv(Path(args.venv)) + else: + py_exe = sys.executable + + # Upgrade pip + console.print("[bold]Upgrading pip in target environment...[/bold]") + cmd_upgrade = [py_exe, "-m", "pip", "install", "--upgrade", "pip"] + if break_system_flag: + cmd_upgrade.append("--break-system-packages") + run(cmd_upgrade) + + # Install GitHub / VCS packages first + console.print(Panel(f"Installing VCS/GitHub packages ({len(VCS_PACKAGES)} items)...", title="Dependencies")) + install_packages(py_exe, VCS_PACKAGES, break_system_flag) + + # Install local Ghost package + console.print(Panel("Installing Ghost main package...", style="purple")) + install_local_package(py_exe, break_system_flag) + + console.print(Panel("[bold green]Installation completed successfully![/bold green]")) + console.print("You can now run: ghost") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2aa89705 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +adb-shell>=0.1 +pex @ git+https://github.com/EntySec/Pex.git +badges @ git+https://github.com/EntySec/Badges.git +colorscript @ git+https://github.com/EntySec/ColorScript.git +rich>=10.0 diff --git a/setup.py b/setup.py index 59dad0c1..15425408 100644 --- a/setup.py +++ b/setup.py @@ -1,52 +1,43 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" +# setup.py — minimal install_requires (keep package install from local source only) +from pathlib import Path from setuptools import setup, find_packages -setup(name='ghost', - version='8.0.0', - description=( - 'Ghost Framework is an Android post-exploitation framework that exploits the' - ' Android Debug Bridge to remotely access an Android device.' - ), - url='http://github.com/EntySec/Ghost', - author='EntySec', - author_email='entysec@gmail.com', - license='MIT', - python_requires='>=3.7.0', - packages=find_packages(), - include_package_data=True, - entry_points={ - "console_scripts": [ - "ghost = ghost:cli" - ] - }, - install_requires=[ - 'adb-shell', - 'pex @ git+https://github.com/EntySec/Pex', - 'badges @ git+https://github.com/EntySec/Badges', - 'colorscript @ git+https://github.com/EntySec/ColorScript' - ], - zip_safe=False - ) +HERE = Path(__file__).parent +readme = (HERE / "README.md").read_text(encoding="utf-8") if (HERE / "README.md").exists() else "" + +setup( + name="ghost", + version="8.0.0", + description="Ghost Framework — Android post-exploitation framework (ADB-based).", + long_description=readme, + long_description_content_type="text/markdown", + url="https://github.com/EntySec/Ghost", + author="EntySec", + author_email="entysec@gmail.com", + license="MIT", + python_requires=">=3.7", + packages=find_packages(exclude=("tests", "docs", "examples")), + include_package_data=True, + zip_safe=False, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Security", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + ], + # Keep runtime install_requires minimal. Put VCS/complex deps into requirements.txt + install_requires=[ + "adb-shell", + ], + entry_points={ + "console_scripts": [ + "ghost = ghost:cli", + ] + }, + project_urls={ + "Source": "https://github.com/EntySec/Ghost", + "Issues": "https://github.com/EntySec/Ghost/issues", + }, +) diff --git a/uninstall.py b/uninstall.py new file mode 100644 index 00000000..d8f555c5 --- /dev/null +++ b/uninstall.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +uninstall.py — Ghost cleanup/uninstall script + +- Deletes virtualenv if exists +- Uninstalls Ghost and related Python packages if installed in system Python +- Adds --break-system-packages for system Python to avoid PEP 668 errors +- Interactive prompts with rich +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from typing import List + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm + +console = Console() +ROOT = Path(__file__).parent.resolve() + +# List of Python packages installed by Ghost installer +PYTHON_PACKAGES = [ + "ghost", + "adb-shell", + "pex", + "badges", + "colorscript", +] + +# Default virtualenv path +DEFAULT_VENV = ROOT / ".venv" + + +def run(cmd: List[str], check: bool = True) -> None: + """Run a shell command and print it.""" + console.log(f"[bold purple]$[/bold purple] {' '.join(cmd)}") + res = subprocess.run(cmd) + if check and res.returncode != 0: + console.print(Panel(f"[red]Command failed: {' '.join(cmd)}[/red]")) + + +def uninstall_packages(python_exe: str, break_system: bool = False) -> None: + """Uninstall all Ghost-related packages.""" + for pkg in PYTHON_PACKAGES: + console.print(Panel(f"Uninstalling package: [bold]{pkg}[/bold]")) + cmd = [python_exe, "-m", "pip", "uninstall", "-y", pkg] + if break_system: + cmd.append("--break-system-packages") + run(cmd) + + +def remove_virtualenv(venv_path: Path) -> None: + """Remove virtualenv folder if exists.""" + if venv_path.exists(): + console.print(Panel(f"Removing virtualenv: [bold]{venv_path}[/bold]")) + import shutil + shutil.rmtree(venv_path) + console.print(Panel(f"[green]Virtualenv removed.[/green]")) + else: + console.print(Panel("[yellow]No virtualenv found.[/yellow]")) + + +def main() -> None: + console.print(Panel("[bold purple]Ghost Uninstaller[/bold purple]\nIndex 99 → Return to Menu / Exit")) + + use_venv = False + if DEFAULT_VENV.exists(): + use_venv = Confirm.ask(f"Detected virtualenv at {DEFAULT_VENV}. Remove it?", default=True) + if use_venv: + remove_virtualenv(DEFAULT_VENV) + + # Ask if want to uninstall packages from system Python + uninstall_system = Confirm.ask("Do you want to uninstall Ghost Python packages from system interpreter?", default=False) + if uninstall_system: + python_exe = sys.executable + uninstall_packages(python_exe, break_system=True) + + console.print(Panel("[bold green]Uninstall process completed.[/bold green]")) + + +if __name__ == "__main__": + main() From b56f121b58713453dbac597311771040bbdb6cd7 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 09:43:55 -0400 Subject: [PATCH 02/47] Remove install.py comments --- install.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/install.py b/install.py index 2ffb708b..640c9c6e 100755 --- a/install.py +++ b/install.py @@ -26,7 +26,6 @@ console = Console() ROOT = Path(__file__).parent.resolve() -# GitHub/URL dependencies VCS_PACKAGES = [ "badges @ git+https://github.com/EntySec/Badges", "pex @ git+https://github.com/EntySec/Pex", @@ -34,7 +33,7 @@ "adb-shell" ] -PY_PACKAGES: List[str] = [] # any pure python packages from requirements.txt +PY_PACKAGES: List[str] = [] DEFAULT_VENV = ROOT / ".venv" @@ -117,18 +116,15 @@ def main() -> None: else: py_exe = sys.executable - # Upgrade pip console.print("[bold]Upgrading pip in target environment...[/bold]") cmd_upgrade = [py_exe, "-m", "pip", "install", "--upgrade", "pip"] if break_system_flag: cmd_upgrade.append("--break-system-packages") run(cmd_upgrade) - # Install GitHub / VCS packages first console.print(Panel(f"Installing VCS/GitHub packages ({len(VCS_PACKAGES)} items)...", title="Dependencies")) install_packages(py_exe, VCS_PACKAGES, break_system_flag) - # Install local Ghost package console.print(Panel("Installing Ghost main package...", style="purple")) install_local_package(py_exe, break_system_flag) From 9c67af9b9674d7f67c0baac31a2c98427c0ac133 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 09:44:50 -0400 Subject: [PATCH 03/47] Remove setup.py comments --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 15425408..02da2725 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -# setup.py — minimal install_requires (keep package install from local source only) - from pathlib import Path from setuptools import setup, find_packages @@ -27,7 +25,6 @@ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ], - # Keep runtime install_requires minimal. Put VCS/complex deps into requirements.txt install_requires=[ "adb-shell", ], From 03e016ce3897874b24422b3a1133ce40ccdef299 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 09:45:17 -0400 Subject: [PATCH 04/47] Remove uninstall.py comments --- uninstall.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/uninstall.py b/uninstall.py index d8f555c5..fb5319b8 100644 --- a/uninstall.py +++ b/uninstall.py @@ -22,7 +22,6 @@ console = Console() ROOT = Path(__file__).parent.resolve() -# List of Python packages installed by Ghost installer PYTHON_PACKAGES = [ "ghost", "adb-shell", @@ -31,7 +30,6 @@ "colorscript", ] -# Default virtualenv path DEFAULT_VENV = ROOT / ".venv" @@ -73,7 +71,6 @@ def main() -> None: if use_venv: remove_virtualenv(DEFAULT_VENV) - # Ask if want to uninstall packages from system Python uninstall_system = Confirm.ask("Do you want to uninstall Ghost Python packages from system interpreter?", default=False) if uninstall_system: python_exe = sys.executable From ef892e81c007274b7c2b19ae522c4d57eac60a6a Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 14:51:33 -0400 Subject: [PATCH 05/47] Device With Rich Ui --- ghost/core/device.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ghost/core/device.py b/ghost/core/device.py index 16fa2201..87cd54b0 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -32,6 +32,18 @@ from pex.fs import FS +# Rich UI imports (visuals only — program logic is NOT modified) +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align + +# Main purple theme +_PURPLE = "#7B61FF" + +_console = Console() + class Device(Cmd, FS): """ Subclass of ghost.core module. @@ -63,6 +75,62 @@ def __init__(self, host: str, port: int = 5555, timeout: int = 10, device=self ) + # ------------------------- + # Rich-based Print Methods + # (Presentation only — do NOT change program logic) + # ------------------------- + def _footer_return_to_menu(self) -> Text: + """Small helper to produce the 'Return to Menu' footer text.""" + t = Text("Index 99 → Exit", style="bold") + t.stylize(f"bold {_PURPLE}") + return t + + def print_process(self, message: str) -> None: + """Display a process/info message using Rich Panel.""" + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_success(self, message: str) -> None: + """Display a success message using Rich Panel.""" + panel = Panel.fit( + Align.left(Text(message)), + title=Text("SUCCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_error(self, message: str) -> None: + """Display an error message using Rich Panel.""" + panel = Panel.fit( + Align.left(Text(message)), + title=Text("ERROR", style="bold white on " + _PURPLE), + border_style="red" + ) + _console.print(panel) + + def print_information(self, message: str) -> None: + """Display an informational message using Rich Table for clarity.""" + table = Table.grid(padding=(0, 1)) + table.add_column(justify="left") + table.add_row(Text(message)) + panel = Panel.fit( + table, + title=Text("INFO", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self) -> None: + """Print an empty line/space using Rich (visual spacer).""" + _console.print() + + # ------------------------- + # Original methods (logic unchanged) + # ------------------------- def get_keys(self) -> tuple: """ Get cryptographic keys. From d3d508d4702c38e19f63c6591544c2c014509d5e Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 14:55:27 -0400 Subject: [PATCH 06/47] Update activity.py and battery.py --- ghost/modules/activity.py | 25 ++++++++++++++++++++++++- ghost/modules/battery.py | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/ghost/modules/activity.py b/ghost/modules/activity.py index 2ebd7348..71c24139 100644 --- a/ghost/modules/activity.py +++ b/ghost/modules/activity.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,24 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + def run(self, _): self.print_process("Getting activity information...") output = self.device.send_command("dumpsys activity") - self.print_empty(output) + self.print_empty(output) \ No newline at end of file diff --git a/ghost/modules/battery.py b/ghost/modules/battery.py index c81ec8d5..289e6a43 100644 --- a/ghost/modules/battery.py +++ b/ghost/modules/battery.py @@ -4,6 +4,14 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,6 +28,22 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + def run(self, _): self.print_process("Getting battery information...") From a56e8252d5de6e4d5ac0ac535b1d90bb7f088a8e Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 14:57:43 -0400 Subject: [PATCH 07/47] Update download.py --- ghost/modules/download.py | 42 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/ghost/modules/download.py b/ghost/modules/download.py index ea030897..faa0f971 100644 --- a/ghost/modules/download.py +++ b/ghost/modules/download.py @@ -4,8 +4,15 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,5 +29,36 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_success(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("SUCCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_error(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("ERROR", style="bold white on " + _PURPLE), + border_style="red" + ) + _console.print(panel) + def run(self, args): - self.device.download(args[1], args[2]) + remote_file, local_path = args[1], args[2] + self.print_process(f"Downloading {remote_file}...") + + success = self.device.download(remote_file, local_path) + if success: + self.print_success(f"File saved to {local_path}") + else: + self.print_error("Download failed!") From d9ef41a32c972caa33d2a4a8d57eb9733a183cff Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 14:58:22 -0400 Subject: [PATCH 08/47] Update keyboard.py --- ghost/modules/keyboard.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ghost/modules/keyboard.py b/ghost/modules/keyboard.py index de044b8f..ea9804e5 100644 --- a/ghost/modules/keyboard.py +++ b/ghost/modules/keyboard.py @@ -6,8 +6,14 @@ import sys import termios import tty - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -24,6 +30,14 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + @staticmethod def get_char(): fd = sys.stdin.fileno() @@ -35,9 +49,9 @@ def get_char(): termios.tcsetattr(fd, termios.TCSADRAIN, old) def run(self, _): - self.print_process("Interacting with keyboard...") - self.print_success("Interactive connection spawned!") + self.print_panel("PROCESS", "Interacting with keyboard...") + self.print_panel("SUCCESS", "Interactive connection spawned!") + self.print_panel("INFO", "Type text below.", color="cyan") - self.print_information("Type text below.") while True: self.device.send_command(f"input text {self.get_char()}") From 3c86bd50806cf78562d990fb2e1ef8b0d678afb3 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:05:45 -0400 Subject: [PATCH 09/47] Update list.py --- ghost/modules/list.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/ghost/modules/list.py b/ghost/modules/list.py index c6685252..fa612b72 100644 --- a/ghost/modules/list.py +++ b/ghost/modules/list.py @@ -4,8 +4,15 @@ """ import datetime - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,15 +29,32 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {_PURPLE}"), + border_style=_PURPLE + ) + _console.print(panel) + + def print_table_rich(self, title: str, headers: tuple, rows: list): + table = Table(title=title, header_style=f"bold {_PURPLE}", border_style=_PURPLE) + for h in headers: + table.add_column(h, style="white", no_wrap=True) + for row in rows: + table.add_row(*[str(i) for i in row]) + _console.print(table) + def run(self, args): - output = self.device.list(args[1]) + self.print_panel("PROCESS", f"Listing directory: {args[1]}") + output = self.device.list(args[1]) if output: headers = ('Name', 'Mode', 'Size', 'Modification Time') - data = list() - + rows = [] for entry in sorted(output): - timestamp = datetime.datetime.fromtimestamp(entry[3]) - data.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) - - self.print_table(f"Directory {args[1]}", headers, *data) + timestamp = datetime.datetime.fromtimestamp(entry[3]).strftime("%Y-%m-%d %H:%M:%S") + rows.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) + self.print_table_rich(f"Directory {args[1]}", headers, rows) + else: + self.print_panel("INFO", "No files found in this directory.") From a1613fa6476fe39167f8dc8eae7184300ceb2759 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:06:33 -0400 Subject: [PATCH 10/47] Update network.py --- ghost/modules/network.py | 81 ++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/ghost/modules/network.py b/ghost/modules/network.py index a84ee2ef..937711b7 100644 --- a/ghost/modules/network.py +++ b/ghost/modules/network.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,96 +27,72 @@ def __init__(self): 'Options': [ ( ('-a', '--arp'), - { - 'help': "Show device ARP table.", - 'action': 'store_true' - } + {'help': "Show device ARP table.", 'action': 'store_true'} ), ( ('-i', '--ipconfig'), - { - 'help': "Show device IP configuration.", - 'action': 'store_true' - } + {'help': "Show device IP configuration.", 'action': 'store_true'} ), ( ('-I', '--iproute'), - { - 'help': "Show device route table.", - 'action': 'store_true' - } + {'help': "Show device route table.", 'action': 'store_true'} ), ( ('-l', '--locate'), - { - 'help': "Show device location.", - 'action': 'store_true' - } + {'help': "Show device location.", 'action': 'store_true'} ), ( ('-s', '--stats'), - { - 'help': "Show device network stats.", - 'action': 'store_true' - } + {'help': "Show device network stats.", 'action': 'store_true'} ), ( ('-p', '--ports'), - { - 'help': "Show device open ports.", - 'action': 'store_true' - } + {'help': "Show device open ports.", 'action': 'store_true'} ), ( ('-S', '--services'), - { - 'help': "Show device services.", - 'action': 'store_true' - } + {'help': "Show device services.", 'action': 'store_true'} ), ( ('-f', '--forwarding'), - { - 'help': "Show device forwarding rules.", - 'action': 'store_true' - } + {'help': "Show device forwarding rules.", 'action': 'store_true'} ) ] }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): outputs = [] if args.arp: - outputs.append( - self.device.send_command('cat /proc/net/arp')) + outputs.append(self.device.send_command('cat /proc/net/arp')) if args.ipconfig: - outputs.append( - self.device.send_command('ip addr show')) + outputs.append(self.device.send_command('ip addr show')) if args.iproute: - outputs.append( - self.device.send_command('ip route show')) + outputs.append(self.device.send_command('ip route show')) if args.locate: - outputs.append( - self.device.send_command('dumpsys location')) + outputs.append(self.device.send_command('dumpsys location')) if args.stats: - outputs.append( - self.device.send_command('cat /proc/net/netstat')) + outputs.append(self.device.send_command('cat /proc/net/netstat')) if args.ports: - outputs.append( - self.device.send_command('busybox netstat -an')) + outputs.append(self.device.send_command('busybox netstat -an')) if args.services: - outputs.append( - self.device.send_command('service list')) + outputs.append(self.device.send_command('service list')) if args.forwarding: - outputs.append( - self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) + outputs.append(self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) - self.print_empty('\n'.join(outputs)) + self.print_panel("NETWORK OUTPUT", '\n'.join(outputs)) From 7c797d8b522e5323adf2d4d0fc676b10789e088f Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:07:14 -0400 Subject: [PATCH 11/47] Update openurl.py --- ghost/modules/openurl.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ghost/modules/openurl.py b/ghost/modules/openurl.py index f6b4b8de..69d774b7 100644 --- a/ghost/modules/openurl.py +++ b/ghost/modules/openurl.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,19 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if not args[1].startswith(("http://", "https://")): - args[1] = "http://" + args[1] + url = args[1] + if not url.startswith(("http://", "https://")): + url = "http://" + url - self.device.send_command(f'am start -a android.intent.action.VIEW -d "{args[1]}"') + self.print_panel("PROCESS", f"Opening URL on device: {url}") + self.device.send_command(f'am start -a android.intent.action.VIEW -d "{url}"') + self.print_panel("SUCCESS", "URL opened successfully!") From b0fee59b8437865aea1c9036200b4cf59d507ad1 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:07:51 -0400 Subject: [PATCH 12/47] Update press.py --- ghost/modules/press.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ghost/modules/press.py b/ghost/modules/press.py index 5235b623..0dfbcd86 100644 --- a/ghost/modules/press.py +++ b/ghost/modules/press.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,19 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if int(args[1]) < 124: - self.device.send_command(f"input keyevent {args[1]}") + keycode = int(args[1]) + if keycode < 124: + self.print_panel("PROCESS", f"Pressing device button with keycode: {keycode}") + self.device.send_command(f"input keyevent {keycode}") + self.print_panel("SUCCESS", "Key event sent successfully!") else: - self.print_error("Invalid keycode!") + self.print_panel("ERROR", "Invalid keycode!", color="red") From 9ab78249f68aecc13ab676fefddc6aa6ae7093bd Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:08:32 -0400 Subject: [PATCH 13/47] Update screenshot.py --- ghost/modules/screenshot.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/ghost/modules/screenshot.py b/ghost/modules/screenshot.py index 6b0b3e8b..7c39be3a 100644 --- a/ghost/modules/screenshot.py +++ b/ghost/modules/screenshot.py @@ -4,8 +4,14 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,9 +28,24 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - self.print_process(f"Taking screenshot...") + local_path = args[1] + self.print_panel("PROCESS", "Taking screenshot on device...") + self.device.send_command("screencap /data/local/tmp/screenshot.png") + success = self.device.download('/data/local/tmp/screenshot.png', local_path) + + if success: + self.print_panel("SUCCESS", f"Screenshot saved to {local_path}") + else: + self.print_panel("ERROR", "Failed to download screenshot!", color="red") - self.device.download('/data/local/tmp/screenshot.png', args[1]) self.device.send_command("rm /data/local/tmp/screenshot.png") From df96442211151075972e09a203486b18cef6f028 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:09:03 -0400 Subject: [PATCH 14/47] Update shell.py --- ghost/modules/shell.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ghost/modules/shell.py b/ghost/modules/shell.py index 35227149..bad6bb45 100644 --- a/ghost/modules/shell.py +++ b/ghost/modules/shell.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,6 +27,16 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - output = self.device.send_command(' '.join(args[1:])) - self.print_empty(output) + command = ' '.join(args[1:]) + self.print_panel("PROCESS", f"Executing shell command: {command}") + output = self.device.send_command(command) + self.print_panel("OUTPUT", output) From 0f90c5b7ea2f3498598853f4b436b34c85a3847e Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:09:36 -0400 Subject: [PATCH 15/47] Update sleep.py --- ghost/modules/sleep.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ghost/modules/sleep.py b/ghost/modules/sleep.py index 049b0547..9b8af222 100644 --- a/ghost/modules/sleep.py +++ b/ghost/modules/sleep.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,5 +27,15 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, _): + self.print_panel("PROCESS", "Putting device into sleep mode...") self.device.send_command("input keyevent 26") + self.print_panel("SUCCESS", "Device is now in sleep mode!") From 75e5baeca6c25487fdf8045a52277d7303a71153 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:10:12 -0400 Subject: [PATCH 16/47] Update upload.py --- ghost/modules/upload.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/ghost/modules/upload.py b/ghost/modules/upload.py index 3bbd4c6d..f28b9c0a 100644 --- a/ghost/modules/upload.py +++ b/ghost/modules/upload.py @@ -4,8 +4,14 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,5 +28,20 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - self.device.upload(args[1], args[2]) + local_file, remote_path = args[1], args[2] + self.print_panel("PROCESS", f"Uploading {local_file} to {remote_path}...") + + success = self.device.upload(local_file, remote_path) + if success: + self.print_panel("SUCCESS", f"File uploaded to {remote_path}") + else: + self.print_panel("ERROR", "Upload failed!", color="red") From d4c42301a866cc3a7fa3bfb00babbca770f55116 Mon Sep 17 00:00:00 2001 From: Modark Date: Thu, 16 Oct 2025 15:10:49 -0400 Subject: [PATCH 17/47] Update wifi.py --- ghost/modules/wifi.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ghost/modules/wifi.py b/ghost/modules/wifi.py index 70b46198..c6dcbf3e 100644 --- a/ghost/modules/wifi.py +++ b/ghost/modules/wifi.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,11 +27,20 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if args[1] in ['on', 'off']: - if args[1] == 'on': - self.device.send_command("svc wifi enable") - else: - self.device.send_command("svc wifi disable") + state = args[1].lower() + if state in ['on', 'off']: + action = "enable" if state == 'on' else "disable" + self.print_panel("PROCESS", f"Turning WiFi {state}...") + self.device.send_command(f"svc wifi {action}") + self.print_panel("SUCCESS", f"WiFi turned {state} successfully!") else: - self.print_usage(self.info['Usage']) + self.print_panel("USAGE", f"Usage: {self.info['Usage']}", color="red") From 72efbab8cb2c6208bb41a21cfcb0de7e06f6d2ac Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 16:23:34 -0400 Subject: [PATCH 18/47] Auto-reconnect --- ghost/core/device.py | 255 +++++++++++++++---------------------------- 1 file changed, 85 insertions(+), 170 deletions(-) diff --git a/ghost/core/device.py b/ghost/core/device.py index 87cd54b0..9788c664 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -23,271 +23,186 @@ """ import os - +import time +import threading from badges.cmd import Cmd - from adb_shell.adb_device import AdbDeviceTcp from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner - from pex.fs import FS - -# Rich UI imports (visuals only — program logic is NOT modified) from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.text import Text from rich.align import Align -# Main purple theme _PURPLE = "#7B61FF" - _console = Console() +_MAX_RETRIES = 5 +_RETRY_DELAY = 3 +_HEALTHCHECK_DELAY = 10 -class Device(Cmd, FS): - """ Subclass of ghost.core module. - - This subclass of ghost.core module is intended for providing - an implementation of device controller. - """ - - def __init__(self, host: str, port: int = 5555, timeout: int = 10, - key_filename: str = 'key') -> None: - """ Initialize device. - :param str host: device host - :param int port: device port - :param int timeout: connection timeout - :param str key_filename: name of the file containing key - :return None: None - """ +class Device(Cmd, FS): + """Enhanced Device class with auto-reconnect and retry mechanisms.""" + def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: self.host = host self.port = int(port) - self.key_file = key_filename self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) + self._reconnect_thread = None + self._stop_reconnect = threading.Event() + super().__init__( prompt=f'(%lineghost%end: %red{self.host}%end)> ', path=[f'{os.path.dirname(os.path.dirname(__file__))}/modules'], device=self ) - # ------------------------- - # Rich-based Print Methods - # (Presentation only — do NOT change program logic) - # ------------------------- - def _footer_return_to_menu(self) -> Text: - """Small helper to produce the 'Return to Menu' footer text.""" - t = Text("Index 99 → Exit", style="bold") - t.stylize(f"bold {_PURPLE}") - return t - def print_process(self, message: str) -> None: - """Display a process/info message using Rich Panel.""" - panel = Panel.fit( - Align.left(Text(message)), - title=Text("PROCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) + _console.print(Panel.fit(Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE)) def print_success(self, message: str) -> None: - """Display a success message using Rich Panel.""" - panel = Panel.fit( - Align.left(Text(message)), - title=Text("SUCCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) + _console.print(Panel.fit(Align.left(Text(message)), + title=Text("SUCCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE)) def print_error(self, message: str) -> None: - """Display an error message using Rich Panel.""" - panel = Panel.fit( - Align.left(Text(message)), - title=Text("ERROR", style="bold white on " + _PURPLE), - border_style="red" - ) - _console.print(panel) + _console.print(Panel.fit(Align.left(Text(message)), + title=Text("ERROR", style="bold white on " + _PURPLE), + border_style="red")) def print_information(self, message: str) -> None: - """Display an informational message using Rich Table for clarity.""" table = Table.grid(padding=(0, 1)) table.add_column(justify="left") table.add_row(Text(message)) - panel = Panel.fit( - table, - title=Text("INFO", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) + _console.print(Panel.fit(table, + title=Text("INFO", style="bold white on " + _PURPLE), + border_style=_PURPLE)) def print_empty(self) -> None: - """Print an empty line/space using Rich (visual spacer).""" _console.print() - # ------------------------- - # Original methods (logic unchanged) - # ------------------------- def get_keys(self) -> tuple: - """ Get cryptographic keys. - - :return tuple: public key, private key - """ - if not os.path.exists(self.key_file): keygen(self.key_file) + with open(self.key_file, 'r') as f: + priv = f.read() + with open(self.key_file + '.pub', 'r') as f: + pub = f.read() + return pub, priv - with open(self.key_file, 'r') as file: - priv = file.read() + def connect(self, auto_reconnect: bool = True) -> bool: + """Connect device with retry and optional auto-reconnect thread.""" + self._stop_reconnect.clear() + self.print_process(f"Connecting to {self.host}...") + keys = self.get_keys() + signer = PythonRSASigner(*keys) - with open(self.key_file + '.pub', 'r') as file: - pub = file.read() - return pub, priv + for attempt in range(1, _MAX_RETRIES + 1): + try: + self.device.connect(rsa_keys=[signer], auth_timeout_s=5) + self.print_success(f"Connected to {self.host}!") - def send_command(self, command: str, output: bool = True) -> str: - """ Send command to the device. + if auto_reconnect: + self._start_auto_reconnect_thread() + return True + except Exception as e: + self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") + if attempt < _MAX_RETRIES: + self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") + time.sleep(_RETRY_DELAY) + + self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") + return False - :param str command: command to send to the device - :param bool output: return command output or not - :return str: empty if output is False otherwise command output - """ + def _start_auto_reconnect_thread(self) -> None: + """Start background thread to monitor and auto-reconnect.""" + if self._reconnect_thread and self._reconnect_thread.is_alive(): + return + self._reconnect_thread = threading.Thread(target=self.auto_reconnect, daemon=True) + self._reconnect_thread.start() + def auto_reconnect(self) -> None: + """Monitor device connection and reconnect if disconnected.""" + while not self._stop_reconnect.is_set(): + try: + self.device.shell("echo ping", transport_timeout_s=3) + except Exception: + self.print_error(f"Lost connection to {self.host}, attempting reconnect...") + if not self.connect(auto_reconnect=False): + self.print_error("Auto-reconnect failed, will retry again...") + else: + self.print_success("Reconnected successfully!") + time.sleep(_HEALTHCHECK_DELAY) + + def disconnect(self) -> None: + """Gracefully disconnect device and stop reconnect thread.""" + self._stop_reconnect.set() + if self._reconnect_thread and self._reconnect_thread.is_alive(): + self._reconnect_thread.join(timeout=2) + + try: + self.device.close() + self.print_success(f"Disconnected from {self.host}.") + except Exception: + self.print_error("Failed to disconnect properly!") + + def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) except Exception: self.print_error("Socket is not connected!") return None - - if output: - return cmd_output - - return "" + return cmd_output if output else "" def list(self, path: str) -> list: - """ List contents of the specified directory. - - :param str path: directory to list contents from - :return list: list of directory contents - """ - try: return self.device.list(path) except Exception: self.print_error("Failed to list directory!") return [] - def connect(self) -> bool: - """ Connect the specified device. - - :return bool: True if connection succeed - """ - - self.print_process(f"Connecting to {self.host}...") - - try: - keys = self.get_keys() - signer = PythonRSASigner(*keys) - - self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - self.print_success(f"Connected to {self.host}!") - - return True - - except Exception: - self.print_error(f"Failed to connect to {self.host}!") - - return False - - def disconnect(self) -> None: - """ Disconnect the specified device. - - :return None: None - """ - - self.device.close() - def download(self, input_file: str, output_path: str) -> bool: - """ Download file from the specified device. - - :param str input_file: path of the file to download - :param str output_path: path to output the file to - :return bool: True if download succeed - """ - exists, is_dir = self.exists(output_path) - if exists: if is_dir: output_path = output_path + '/' + os.path.split(input_file)[1] - try: self.print_process(f"Downloading {input_file}...") self.device.pull(input_file, output_path) - - self.print_process(f"Saving to {output_path}...") self.print_success(f"Saved to {output_path}!") - return True - except Exception: - self.print_error(f"Remote file: {input_file}: does not exist or a directory!") - + self.print_error(f"Remote file {input_file} not found or invalid!") return False def upload(self, input_file: str, output_path: str) -> bool: - """ Upload file to the specified device. - - :param str input_file: path of the file to upload - :param str output_path: path to output the file to - :return bool: True if upload succeed - """ - if self.check_file(input_file): try: self.print_process(f"Uploading {input_file}...") self.device.push(input_file, output_path) - - self.print_process(f"Saving to {output_path}...") self.print_success(f"Saved to {output_path}!") - return True - except Exception: try: output_path = output_path + '/' + os.path.split(input_file)[1] self.device.push(input_file, output_path) - except Exception: - self.print_error(f"Remote directory: {output_path}: does not exist!") - + self.print_error(f"Remote directory {output_path} does not exist!") return False def is_rooted(self) -> bool: - """ Check if the specified device is rooted. - - :return bool: True if device is rooted - """ - responder = self.send_command('which su') - - if not responder or responder.isspace(): - return False - - return True + return bool(responder and not responder.isspace()) def interact(self) -> None: - """ Interact with the specified device. - - :return None: None - """ - self.print_success("Interactive connection spawned!") - - self.print_empty() self.print_process("Loading device modules...") - self.print_information(f"Modules loaded: {str(len(self.external))}") self.loop() From 396df9396f77440d22e58158a1ae448d0e99dbd0 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 16:31:23 -0400 Subject: [PATCH 19/47] Add Device Analyzer & Real-Time Log Viewer --- ghost/core/device.py | 115 +++++++++++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 38 deletions(-) diff --git a/ghost/core/device.py b/ghost/core/device.py index 9788c664..d8d364d4 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -30,6 +30,7 @@ from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner from pex.fs import FS + from rich.console import Console from rich.panel import Panel from rich.table import Table @@ -40,12 +41,12 @@ _console = Console() _MAX_RETRIES = 5 -_RETRY_DELAY = 3 -_HEALTHCHECK_DELAY = 10 +_RETRY_DELAY = 3 +_HEALTHCHECK_DELAY = 10 class Device(Cmd, FS): - """Enhanced Device class with auto-reconnect and retry mechanisms.""" + """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: self.host = host @@ -62,32 +63,23 @@ def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: device=self ) - def print_process(self, message: str) -> None: - _console.print(Panel.fit(Align.left(Text(message)), - title=Text("PROCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE)) - - def print_success(self, message: str) -> None: - _console.print(Panel.fit(Align.left(Text(message)), - title=Text("SUCCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE)) - - def print_error(self, message: str) -> None: + # ------------------------- + # Rich Print Helpers + # ------------------------- + def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: _console.print(Panel.fit(Align.left(Text(message)), - title=Text("ERROR", style="bold white on " + _PURPLE), - border_style="red")) - - def print_information(self, message: str) -> None: - table = Table.grid(padding=(0, 1)) - table.add_column(justify="left") - table.add_row(Text(message)) - _console.print(Panel.fit(table, - title=Text("INFO", style="bold white on " + _PURPLE), - border_style=_PURPLE)) - - def print_empty(self) -> None: - _console.print() - + title=Text(title, style=f"bold white on {color}"), + border_style=color)) + + def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) + def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") + def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") + def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) + def print_empty(self): _console.print() + + # ------------------------- + # Device Keys + # ------------------------- def get_keys(self) -> tuple: if not os.path.exists(self.key_file): keygen(self.key_file) @@ -97,8 +89,10 @@ def get_keys(self) -> tuple: pub = f.read() return pub, priv + # ------------------------- + # Connection Management + # ------------------------- def connect(self, auto_reconnect: bool = True) -> bool: - """Connect device with retry and optional auto-reconnect thread.""" self._stop_reconnect.clear() self.print_process(f"Connecting to {self.host}...") keys = self.get_keys() @@ -122,14 +116,11 @@ def connect(self, auto_reconnect: bool = True) -> bool: return False def _start_auto_reconnect_thread(self) -> None: - """Start background thread to monitor and auto-reconnect.""" - if self._reconnect_thread and self._reconnect_thread.is_alive(): - return + if self._reconnect_thread and self._reconnect_thread.is_alive(): return self._reconnect_thread = threading.Thread(target=self.auto_reconnect, daemon=True) self._reconnect_thread.start() def auto_reconnect(self) -> None: - """Monitor device connection and reconnect if disconnected.""" while not self._stop_reconnect.is_set(): try: self.device.shell("echo ping", transport_timeout_s=3) @@ -142,17 +133,18 @@ def auto_reconnect(self) -> None: time.sleep(_HEALTHCHECK_DELAY) def disconnect(self) -> None: - """Gracefully disconnect device and stop reconnect thread.""" self._stop_reconnect.set() if self._reconnect_thread and self._reconnect_thread.is_alive(): self._reconnect_thread.join(timeout=2) - try: self.device.close() self.print_success(f"Disconnected from {self.host}.") except Exception: self.print_error("Failed to disconnect properly!") + # ------------------------- + # Command / File Operations + # ------------------------- def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) @@ -171,8 +163,7 @@ def list(self, path: str) -> list: def download(self, input_file: str, output_path: str) -> bool: exists, is_dir = self.exists(output_path) if exists: - if is_dir: - output_path = output_path + '/' + os.path.split(input_file)[1] + if is_dir: output_path += '/' + os.path.split(input_file)[1] try: self.print_process(f"Downloading {input_file}...") self.device.pull(input_file, output_path) @@ -191,7 +182,7 @@ def upload(self, input_file: str, output_path: str) -> bool: return True except Exception: try: - output_path = output_path + '/' + os.path.split(input_file)[1] + output_path += '/' + os.path.split(input_file)[1] self.device.push(input_file, output_path) except Exception: self.print_error(f"Remote directory {output_path} does not exist!") @@ -206,3 +197,51 @@ def interact(self) -> None: self.print_process("Loading device modules...") self.print_information(f"Modules loaded: {str(len(self.external))}") self.loop() + + # ------------------------- + # Device Analyzer + # ------------------------- + def analyze_device(self): + self.print_process(f"Analyzing {self.host} ...") + try: + props = { + "Manufacturer": self.send_command("getprop ro.product.manufacturer"), + "Model": self.send_command("getprop ro.product.model"), + "Android Version": self.send_command("getprop ro.build.version.release"), + "Security Patch": self.send_command("getprop ro.build.version.security_patch"), + "Architecture": self.send_command("getprop ro.product.cpu.abi"), + "Rooted": "Yes" if self.is_rooted() else "No" + } + + table = Table(title=f"📱 Device Analysis — {self.host}", border_style=_PURPLE) + table.add_column("Property", style="bold white") + table.add_column("Value", style="dim") + + for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") + _console.print(table) + self.print_success("Analysis complete!") + except Exception as e: + self.print_error(f"Analysis failed: {e}") + + # ------------------------- + # Real-Time Log Viewer + # ------------------------- + def live_logcat(self): + self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") + + def stream_logs(): + try: + shell = self.device.shell("logcat -v time", decode=False) + for line in shell: + decoded = line.decode(errors="ignore").strip() + if " E " in decoded: _console.print(Text(decoded, style="red")) + elif " W " in decoded: _console.print(Text(decoded, style="yellow")) + else: _console.print(Text(decoded, style="dim")) + except KeyboardInterrupt: + self.print_process("Log streaming stopped by user.") + except Exception as e: + self.print_error(f"Logcat error: {e}") + + t = threading.Thread(target=stream_logs) + t.daemon = True + t.start() From e0f9a1e65bcc41f3d42977188d1f2f14b434de67 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 16:36:10 -0400 Subject: [PATCH 20/47] Add help command --- ghost/core/console.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index 3e62ece1..b8ba6fc4 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -82,11 +82,10 @@ def __init__(self) -> None: # Visual helpers (only) # ------------------------- def _render_header(self) -> None: - """Render a fancy hacker-style header with tools table.""" + """Render a fancy hacker-style header with tools table and quick help.""" title = Text("Ghost Framework 8.0.0", style="bold white") subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") - # ASCII art panel (kept similar to original) ascii_art = Text( " .--. .-. .-.\n" " : .--': : .' `.\n" @@ -101,25 +100,25 @@ def _render_header(self) -> None: border_style=PURPLE, box=box.HEAVY, padding=(0, 2), - title="[bold]" + "GHOST", + title="[bold]GHOST", subtitle=title, ) - # Tools / Commands table (visual only) - tools_table = Table.grid(expand=True) - tools_table.add_column(ratio=1) - tools_table.add_column(ratio=2) - - tools = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) - tools.add_column("Command", style="bold white", no_wrap=True) - tools.add_column("Description", style="dim") - - tools.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") - tools.add_row("devices", "List connected devices") - tools.add_row("disconnect ", "Disconnect device by ID") - tools.add_row("interact ", "Interact with a connected device") - tools.add_row("exit", "Quit Ghost Framework") - tools.add_row("Index 99", "Return to Menu / Exit (UI helper)") + # Quick Help / Shortcuts table + quick_help = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + quick_help.add_column("Command / Alias", style="bold white", no_wrap=True) + quick_help.add_column("Description", style="dim") + + quick_help.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") + quick_help.add_row("devices", "List connected devices") + quick_help.add_row("disconnect ", "Disconnect device by ID") + quick_help.add_row("interact ", "Interact with a connected device") + quick_help.add_row("analyze ", "Run Device Analyzer") + quick_help.add_row("an ", "Alias for analyze") + quick_help.add_row("logcat ", "Start live logcat stream") + quick_help.add_row("lc ", "Alias for logcat") + quick_help.add_row("exit", "Quit Ghost Framework") + quick_help.add_row("Index 99", "Return to Menu / Exit (UI helper)") right_panel = Panel( Align.left( @@ -131,13 +130,13 @@ def _render_header(self) -> None: title="[bold]Info", ) - # combine header columns - header_columns = Columns([left, Panel(Padding(tools, (1, 2)), border_style=PURPLE), right_panel]) + header_columns = Columns([left, Panel(Padding(quick_help, (1, 2)), border_style=PURPLE), right_panel]) self.rich.print(header_columns) self.rich.print(Rule(style=PURPLE)) self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) self.rich.print() + def print_empty(self, message: str = "") -> None: # preserve signature and behavior, but print a small spacer self.rich.print("") # purely visual @@ -290,7 +289,7 @@ def do_interact(self, args: list) -> None: self.print_process(f"Interacting with device {str(device_id)}...") self.devices[device_id]['device'].interact() - + def shell(self) -> None: """ Run console shell. From 573020a0c63327205117530b8e870b5def431f4e Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 16:38:05 -0400 Subject: [PATCH 21/47] Remove setup.py --- ghost/__init__.py | 35 ----- ghost/core/__init__.py | 23 --- ghost/core/console.py | 299 ------------------------------------ ghost/core/device.py | 247 ----------------------------- ghost/modules/activity.py | 50 ------ ghost/modules/battery.py | 51 ------ ghost/modules/download.py | 64 -------- ghost/modules/keyboard.py | 57 ------- ghost/modules/list.py | 60 -------- ghost/modules/network.py | 98 ------------ ghost/modules/openurl.py | 45 ------ ghost/modules/press.py | 45 ------ ghost/modules/screenshot.py | 51 ------ ghost/modules/shell.py | 42 ----- ghost/modules/sleep.py | 41 ----- ghost/modules/upload.py | 47 ------ ghost/modules/wifi.py | 46 ------ 17 files changed, 1301 deletions(-) delete mode 100644 ghost/__init__.py delete mode 100644 ghost/core/__init__.py delete mode 100644 ghost/core/console.py delete mode 100644 ghost/core/device.py delete mode 100644 ghost/modules/activity.py delete mode 100644 ghost/modules/battery.py delete mode 100644 ghost/modules/download.py delete mode 100644 ghost/modules/keyboard.py delete mode 100644 ghost/modules/list.py delete mode 100644 ghost/modules/network.py delete mode 100644 ghost/modules/openurl.py delete mode 100644 ghost/modules/press.py delete mode 100644 ghost/modules/screenshot.py delete mode 100644 ghost/modules/shell.py delete mode 100644 ghost/modules/sleep.py delete mode 100644 ghost/modules/upload.py delete mode 100644 ghost/modules/wifi.py diff --git a/ghost/__init__.py b/ghost/__init__.py deleted file mode 100644 index 5cc61709..00000000 --- a/ghost/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from ghost.core.console import Console - - -def cli() -> None: - """ Ghost Framework command-line interface. - - :return None: None - """ - - console = Console() - console.shell() diff --git a/ghost/core/__init__.py b/ghost/core/__init__.py deleted file mode 100644 index e1ed48b4..00000000 --- a/ghost/core/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" diff --git a/ghost/core/console.py b/ghost/core/console.py deleted file mode 100644 index b8ba6fc4..00000000 --- a/ghost/core/console.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from badges.cmd import Cmd - -from ghost.core.device import Device - -# --- Rich UI imports for styling (visual-only; logic untouched) --- -from rich.console import Console as RichConsole -from rich.panel import Panel -from rich.table import Table -from rich.text import Text -from rich.align import Align -from rich import box -from rich.style import Style -from rich.rule import Rule -from rich.padding import Padding -from rich.columns import Columns -from rich.markdown import Markdown - -# Theme colors -PURPLE = "#7B61FF" -WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) -INFO_STYLE = Style(color=PURPLE, bold=True) -WARN_STYLE = Style(color="yellow", bold=True) -ERR_STYLE = Style(color="red", bold=True) -SUCCESS_STYLE = Style(color="green", bold=True) - - -class Console(Cmd): - """ Subclass of ghost.core module. - - This subclass of ghost.core modules is intended for providing - main Ghost Framework console interface. - """ - - def __init__(self) -> None: - # keep original prompt/intro tokens so framework behavior is unchanged - super().__init__( - prompt='(%lineghost%end)> ', - intro="""%clear%end - .--. .-. .-. - : .--': : .' `. - : : _ : `-. .--. .--.`. .' - : :; :: .. :' .; :`._-.': : - `.__.':_;:_;`.__.'`.__.':_; - ---=[ %bold%whiteGhost Framework 8.0.0%end ---=[ Developed by EntySec (%linehttps://entysec.com/%end) -""" - ) - - # preserve devices dict & logic - self.devices = {} - - # create rich console for all visual outputs - self.rich = RichConsole() - self._render_header() - - # ------------------------- - # Visual helpers (only) - # ------------------------- - def _render_header(self) -> None: - """Render a fancy hacker-style header with tools table and quick help.""" - title = Text("Ghost Framework 8.0.0", style="bold white") - subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") - - ascii_art = Text( - " .--. .-. .-.\n" - " : .--': : .' `.\n" - " : : _ : `-. .--. .--.`. .'\n" - " : :; :: .. :' .; :`._-.': :\n" - " `.__.':_;:_;`.__.'`.__.':_;", - justify="center", - ) - - left = Panel( - Align.center(ascii_art), - border_style=PURPLE, - box=box.HEAVY, - padding=(0, 2), - title="[bold]GHOST", - subtitle=title, - ) - - # Quick Help / Shortcuts table - quick_help = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) - quick_help.add_column("Command / Alias", style="bold white", no_wrap=True) - quick_help.add_column("Description", style="dim") - - quick_help.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") - quick_help.add_row("devices", "List connected devices") - quick_help.add_row("disconnect ", "Disconnect device by ID") - quick_help.add_row("interact ", "Interact with a connected device") - quick_help.add_row("analyze ", "Run Device Analyzer") - quick_help.add_row("an ", "Alias for analyze") - quick_help.add_row("logcat ", "Start live logcat stream") - quick_help.add_row("lc ", "Alias for logcat") - quick_help.add_row("exit", "Quit Ghost Framework") - quick_help.add_row("Index 99", "Return to Menu / Exit (UI helper)") - - right_panel = Panel( - Align.left( - Text.assemble(subtitle, "\n\n", "Theme: ", (PURPLE, "Hacker • Purple")) - ), - border_style=PURPLE, - box=box.ROUNDED, - padding=(0, 1), - title="[bold]Info", - ) - - header_columns = Columns([left, Panel(Padding(quick_help, (1, 2)), border_style=PURPLE), right_panel]) - self.rich.print(header_columns) - self.rich.print(Rule(style=PURPLE)) - self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) - self.rich.print() - - - def print_empty(self, message: str = "") -> None: - # preserve signature and behavior, but print a small spacer - self.rich.print("") # purely visual - - def print_information(self, message: str) -> None: - # visually enhanced info box (preserves semantics) - self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) - - def print_warning(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) - - def print_error(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) - - def print_success(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) - - def print_usage(self, usage: str) -> None: - # show usage in an emphasized box - usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) - footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) - self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) - - def print_process(self, message: str) -> None: - # lightweight status indicator (visual only) - with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): - # no blocking logic here — just visual affordance - pass - - def print_table(self, title: str, columns: tuple, *rows) -> None: - """Render a stylized table for lists like connected devices.""" - table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) - for col in columns: - table.add_column(str(col), header_style="bold white") - for row in rows: - # ensure each item is string (matching original behavior) - table.add_row(*[str(x) for x in row]) - footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) - wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) - self.rich.print(wrapper) - - # ------------------------- - # Original command logic (UNCHANGED) - # ------------------------- - def do_exit(self, _) -> None: - """ Exit Ghost Framework. - - :return None: None - :raises EOFError: EOF error - """ - - for device in list(self.devices): - self.devices[device]['device'].disconnect() - del self.devices[device] - - raise EOFError - - def do_connect(self, args: list) -> None: - """ Connect device. - - :param list args: arguments - :return None: None - """ - - if len(args) < 2: - self.print_usage("connect :[port]") - return - - address = args[1].split(':') - - if len(address) < 2: - host, port = address[0], 5555 - else: - host, port = address[0], int(address[1]) - - device = Device(host=host, port=port) - - if device.connect(): - self.devices.update({ - len(self.devices): { - 'host': host, - 'port': str(port), - 'device': device - } - }) - self.print_empty("") - - self.print_information( - f"Type %greendevices%end to list all connected devices.") - self.print_information( - f"Type %greeninteract {str(len(self.devices) - 1)}%end " - "to interact this device." - ) - - def do_devices(self, _) -> None: - """ Show connected devices. - - :return None: None - """ - - if not self.devices: - self.print_warning("No devices connected.") - return - - devices = [] - - for device in self.devices: - devices.append( - (device, self.devices[device]['host'], - self.devices[device]['port'])) - - self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) - - def do_disconnect(self, args: list) -> None: - """ Disconnect device. - - :param list args: arguments - :return None: None - """ - - if len(args) < 2: - self.print_usage("disconnect ") - return - - device_id = int(args[1]) - - if device_id not in self.devices: - self.print_error("Invalid device ID!") - return - - self.devices[device_id]['device'].disconnect() - self.devices.pop(device_id) - - def do_interact(self, args: list) -> None: - """ Interact with device. - - :param list args: arguments - :return None: None - """ - - if len(args) < 2: - self.print_usage("interact ") - return - - device_id = int(args[1]) - - if device_id not in self.devices: - self.print_error("Invalid device ID!") - return - - self.print_process(f"Interacting with device {str(device_id)}...") - self.devices[device_id]['device'].interact() - - def shell(self) -> None: - """ Run console shell. - - :return None: None - """ - - self.loop() diff --git a/ghost/core/device.py b/ghost/core/device.py deleted file mode 100644 index d8d364d4..00000000 --- a/ghost/core/device.py +++ /dev/null @@ -1,247 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import os -import time -import threading -from badges.cmd import Cmd -from adb_shell.adb_device import AdbDeviceTcp -from adb_shell.auth.keygen import keygen -from adb_shell.auth.sign_pythonrsa import PythonRSASigner -from pex.fs import FS - -from rich.console import Console -from rich.panel import Panel -from rich.table import Table -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - -_MAX_RETRIES = 5 -_RETRY_DELAY = 3 -_HEALTHCHECK_DELAY = 10 - - -class Device(Cmd, FS): - """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" - - def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: - self.host = host - self.port = int(port) - self.key_file = key_filename - self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) - - self._reconnect_thread = None - self._stop_reconnect = threading.Event() - - super().__init__( - prompt=f'(%lineghost%end: %red{self.host}%end)> ', - path=[f'{os.path.dirname(os.path.dirname(__file__))}/modules'], - device=self - ) - - # ------------------------- - # Rich Print Helpers - # ------------------------- - def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: - _console.print(Panel.fit(Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color)) - - def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) - def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") - def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") - def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) - def print_empty(self): _console.print() - - # ------------------------- - # Device Keys - # ------------------------- - def get_keys(self) -> tuple: - if not os.path.exists(self.key_file): - keygen(self.key_file) - with open(self.key_file, 'r') as f: - priv = f.read() - with open(self.key_file + '.pub', 'r') as f: - pub = f.read() - return pub, priv - - # ------------------------- - # Connection Management - # ------------------------- - def connect(self, auto_reconnect: bool = True) -> bool: - self._stop_reconnect.clear() - self.print_process(f"Connecting to {self.host}...") - keys = self.get_keys() - signer = PythonRSASigner(*keys) - - for attempt in range(1, _MAX_RETRIES + 1): - try: - self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - self.print_success(f"Connected to {self.host}!") - - if auto_reconnect: - self._start_auto_reconnect_thread() - return True - except Exception as e: - self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") - if attempt < _MAX_RETRIES: - self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") - time.sleep(_RETRY_DELAY) - - self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") - return False - - def _start_auto_reconnect_thread(self) -> None: - if self._reconnect_thread and self._reconnect_thread.is_alive(): return - self._reconnect_thread = threading.Thread(target=self.auto_reconnect, daemon=True) - self._reconnect_thread.start() - - def auto_reconnect(self) -> None: - while not self._stop_reconnect.is_set(): - try: - self.device.shell("echo ping", transport_timeout_s=3) - except Exception: - self.print_error(f"Lost connection to {self.host}, attempting reconnect...") - if not self.connect(auto_reconnect=False): - self.print_error("Auto-reconnect failed, will retry again...") - else: - self.print_success("Reconnected successfully!") - time.sleep(_HEALTHCHECK_DELAY) - - def disconnect(self) -> None: - self._stop_reconnect.set() - if self._reconnect_thread and self._reconnect_thread.is_alive(): - self._reconnect_thread.join(timeout=2) - try: - self.device.close() - self.print_success(f"Disconnected from {self.host}.") - except Exception: - self.print_error("Failed to disconnect properly!") - - # ------------------------- - # Command / File Operations - # ------------------------- - def send_command(self, command: str, output: bool = True) -> str: - try: - cmd_output = self.device.shell(command) - except Exception: - self.print_error("Socket is not connected!") - return None - return cmd_output if output else "" - - def list(self, path: str) -> list: - try: - return self.device.list(path) - except Exception: - self.print_error("Failed to list directory!") - return [] - - def download(self, input_file: str, output_path: str) -> bool: - exists, is_dir = self.exists(output_path) - if exists: - if is_dir: output_path += '/' + os.path.split(input_file)[1] - try: - self.print_process(f"Downloading {input_file}...") - self.device.pull(input_file, output_path) - self.print_success(f"Saved to {output_path}!") - return True - except Exception: - self.print_error(f"Remote file {input_file} not found or invalid!") - return False - - def upload(self, input_file: str, output_path: str) -> bool: - if self.check_file(input_file): - try: - self.print_process(f"Uploading {input_file}...") - self.device.push(input_file, output_path) - self.print_success(f"Saved to {output_path}!") - return True - except Exception: - try: - output_path += '/' + os.path.split(input_file)[1] - self.device.push(input_file, output_path) - except Exception: - self.print_error(f"Remote directory {output_path} does not exist!") - return False - - def is_rooted(self) -> bool: - responder = self.send_command('which su') - return bool(responder and not responder.isspace()) - - def interact(self) -> None: - self.print_success("Interactive connection spawned!") - self.print_process("Loading device modules...") - self.print_information(f"Modules loaded: {str(len(self.external))}") - self.loop() - - # ------------------------- - # Device Analyzer - # ------------------------- - def analyze_device(self): - self.print_process(f"Analyzing {self.host} ...") - try: - props = { - "Manufacturer": self.send_command("getprop ro.product.manufacturer"), - "Model": self.send_command("getprop ro.product.model"), - "Android Version": self.send_command("getprop ro.build.version.release"), - "Security Patch": self.send_command("getprop ro.build.version.security_patch"), - "Architecture": self.send_command("getprop ro.product.cpu.abi"), - "Rooted": "Yes" if self.is_rooted() else "No" - } - - table = Table(title=f"📱 Device Analysis — {self.host}", border_style=_PURPLE) - table.add_column("Property", style="bold white") - table.add_column("Value", style="dim") - - for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") - _console.print(table) - self.print_success("Analysis complete!") - except Exception as e: - self.print_error(f"Analysis failed: {e}") - - # ------------------------- - # Real-Time Log Viewer - # ------------------------- - def live_logcat(self): - self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") - - def stream_logs(): - try: - shell = self.device.shell("logcat -v time", decode=False) - for line in shell: - decoded = line.decode(errors="ignore").strip() - if " E " in decoded: _console.print(Text(decoded, style="red")) - elif " W " in decoded: _console.print(Text(decoded, style="yellow")) - else: _console.print(Text(decoded, style="dim")) - except KeyboardInterrupt: - self.print_process("Log streaming stopped by user.") - except Exception as e: - self.print_error(f"Logcat error: {e}") - - t = threading.Thread(target=stream_logs) - t.daemon = True - t.start() diff --git a/ghost/modules/activity.py b/ghost/modules/activity.py deleted file mode 100644 index 71c24139..00000000 --- a/ghost/modules/activity.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "settings", - 'Name': "activity", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Show device activity information.", - 'Usage': "activity", - 'MinArgs': 0, - 'NeedsRoot': False - }) - - def print_process(self, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text("PROCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def print_empty(self, message: str = ""): - panel = Panel.fit( - Align.left(Text(message if message else "")), - title=Text("OUTPUT", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def run(self, _): - self.print_process("Getting activity information...") - - output = self.device.send_command("dumpsys activity") - self.print_empty(output) \ No newline at end of file diff --git a/ghost/modules/battery.py b/ghost/modules/battery.py deleted file mode 100644 index 289e6a43..00000000 --- a/ghost/modules/battery.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -# Main theme color -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "settings", - 'Name': "battery", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Show device battery information.", - 'Usage': "battery", - 'MinArgs': 0, - 'NeedsRoot': False - }) - - def print_process(self, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text("PROCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def print_empty(self, message: str = ""): - panel = Panel.fit( - Align.left(Text(message if message else "No output.")), - title=Text("OUTPUT", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def run(self, _): - self.print_process("Getting battery information...") - - output = self.device.send_command("dumpsys battery") - self.print_empty(output) diff --git a/ghost/modules/download.py b/ghost/modules/download.py deleted file mode 100644 index faa0f971..00000000 --- a/ghost/modules/download.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -import os -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -# Main theme color -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "download", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Download file from device.", - 'Usage': "download ", - 'MinArgs': 2, - 'NeedsRoot': False - }) - - def print_process(self, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text("PROCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def print_success(self, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text("SUCCESS", style="bold white on " + _PURPLE), - border_style=_PURPLE - ) - _console.print(panel) - - def print_error(self, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text("ERROR", style="bold white on " + _PURPLE), - border_style="red" - ) - _console.print(panel) - - def run(self, args): - remote_file, local_path = args[1], args[2] - self.print_process(f"Downloading {remote_file}...") - - success = self.device.download(remote_file, local_path) - if success: - self.print_success(f"File saved to {local_path}") - else: - self.print_error("Download failed!") diff --git a/ghost/modules/keyboard.py b/ghost/modules/keyboard.py deleted file mode 100644 index ea9804e5..00000000 --- a/ghost/modules/keyboard.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -import sys -import termios -import tty -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "keyboard", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Interact with device keyboard.", - 'Usage': "keyboard", - 'MinArgs': 0, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - @staticmethod - def get_char(): - fd = sys.stdin.fileno() - old = termios.tcgetattr(fd) - try: - tty.setraw(fd) - return sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old) - - def run(self, _): - self.print_panel("PROCESS", "Interacting with keyboard...") - self.print_panel("SUCCESS", "Interactive connection spawned!") - self.print_panel("INFO", "Type text below.", color="cyan") - - while True: - self.device.send_command(f"input text {self.get_char()}") diff --git a/ghost/modules/list.py b/ghost/modules/list.py deleted file mode 100644 index fa612b72..00000000 --- a/ghost/modules/list.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -import datetime -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.table import Table -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "list", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "List directory contents.", - 'Usage': "list ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {_PURPLE}"), - border_style=_PURPLE - ) - _console.print(panel) - - def print_table_rich(self, title: str, headers: tuple, rows: list): - table = Table(title=title, header_style=f"bold {_PURPLE}", border_style=_PURPLE) - for h in headers: - table.add_column(h, style="white", no_wrap=True) - for row in rows: - table.add_row(*[str(i) for i in row]) - _console.print(table) - - def run(self, args): - self.print_panel("PROCESS", f"Listing directory: {args[1]}") - - output = self.device.list(args[1]) - if output: - headers = ('Name', 'Mode', 'Size', 'Modification Time') - rows = [] - for entry in sorted(output): - timestamp = datetime.datetime.fromtimestamp(entry[3]).strftime("%Y-%m-%d %H:%M:%S") - rows.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) - self.print_table_rich(f"Directory {args[1]}", headers, rows) - else: - self.print_panel("INFO", "No files found in this directory.") diff --git a/ghost/modules/network.py b/ghost/modules/network.py deleted file mode 100644 index 937711b7..00000000 --- a/ghost/modules/network.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "network", - 'Authors': [ - 'Loubaris - module developer' - ], - 'Description': "Retrieve network informations.", - 'NeedsRoot': False, - 'MinArgs': 1, - 'Options': [ - ( - ('-a', '--arp'), - {'help': "Show device ARP table.", 'action': 'store_true'} - ), - ( - ('-i', '--ipconfig'), - {'help': "Show device IP configuration.", 'action': 'store_true'} - ), - ( - ('-I', '--iproute'), - {'help': "Show device route table.", 'action': 'store_true'} - ), - ( - ('-l', '--locate'), - {'help': "Show device location.", 'action': 'store_true'} - ), - ( - ('-s', '--stats'), - {'help': "Show device network stats.", 'action': 'store_true'} - ), - ( - ('-p', '--ports'), - {'help': "Show device open ports.", 'action': 'store_true'} - ), - ( - ('-S', '--services'), - {'help': "Show device services.", 'action': 'store_true'} - ), - ( - ('-f', '--forwarding'), - {'help': "Show device forwarding rules.", 'action': 'store_true'} - ) - ] - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message if message else "No output.")), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - outputs = [] - - if args.arp: - outputs.append(self.device.send_command('cat /proc/net/arp')) - - if args.ipconfig: - outputs.append(self.device.send_command('ip addr show')) - - if args.iproute: - outputs.append(self.device.send_command('ip route show')) - - if args.locate: - outputs.append(self.device.send_command('dumpsys location')) - - if args.stats: - outputs.append(self.device.send_command('cat /proc/net/netstat')) - - if args.ports: - outputs.append(self.device.send_command('busybox netstat -an')) - - if args.services: - outputs.append(self.device.send_command('service list')) - - if args.forwarding: - outputs.append(self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) - - self.print_panel("NETWORK OUTPUT", '\n'.join(outputs)) diff --git a/ghost/modules/openurl.py b/ghost/modules/openurl.py deleted file mode 100644 index 69d774b7..00000000 --- a/ghost/modules/openurl.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "openurl", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Open URL on device.", - 'Usage': "openurl ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - url = args[1] - if not url.startswith(("http://", "https://")): - url = "http://" + url - - self.print_panel("PROCESS", f"Opening URL on device: {url}") - self.device.send_command(f'am start -a android.intent.action.VIEW -d "{url}"') - self.print_panel("SUCCESS", "URL opened successfully!") diff --git a/ghost/modules/press.py b/ghost/modules/press.py deleted file mode 100644 index 0dfbcd86..00000000 --- a/ghost/modules/press.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "press", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Press device button by keycode.", - 'Usage': "press ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - keycode = int(args[1]) - if keycode < 124: - self.print_panel("PROCESS", f"Pressing device button with keycode: {keycode}") - self.device.send_command(f"input keyevent {keycode}") - self.print_panel("SUCCESS", "Key event sent successfully!") - else: - self.print_panel("ERROR", "Invalid keycode!", color="red") diff --git a/ghost/modules/screenshot.py b/ghost/modules/screenshot.py deleted file mode 100644 index 7c39be3a..00000000 --- a/ghost/modules/screenshot.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -import os -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "screenshot", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Take device screenshot.", - 'Usage': "screenshot ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - local_path = args[1] - self.print_panel("PROCESS", "Taking screenshot on device...") - - self.device.send_command("screencap /data/local/tmp/screenshot.png") - success = self.device.download('/data/local/tmp/screenshot.png', local_path) - - if success: - self.print_panel("SUCCESS", f"Screenshot saved to {local_path}") - else: - self.print_panel("ERROR", "Failed to download screenshot!", color="red") - - self.device.send_command("rm /data/local/tmp/screenshot.png") diff --git a/ghost/modules/shell.py b/ghost/modules/shell.py deleted file mode 100644 index bad6bb45..00000000 --- a/ghost/modules/shell.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "shell", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Execute shell command on device.", - 'Usage': "shell ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message if message else "No output.")), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - command = ' '.join(args[1:]) - self.print_panel("PROCESS", f"Executing shell command: {command}") - output = self.device.send_command(command) - self.print_panel("OUTPUT", output) diff --git a/ghost/modules/sleep.py b/ghost/modules/sleep.py deleted file mode 100644 index 9b8af222..00000000 --- a/ghost/modules/sleep.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "sleep", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Put device into sleep mode.", - 'Usage': "sleep", - 'MinArgs': 0, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, _): - self.print_panel("PROCESS", "Putting device into sleep mode...") - self.device.send_command("input keyevent 26") - self.print_panel("SUCCESS", "Device is now in sleep mode!") diff --git a/ghost/modules/upload.py b/ghost/modules/upload.py deleted file mode 100644 index f28b9c0a..00000000 --- a/ghost/modules/upload.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -import os -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "manage", - 'Name': "upload", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Upload file to device.", - 'Usage': "upload ", - 'MinArgs': 2, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - local_file, remote_path = args[1], args[2] - self.print_panel("PROCESS", f"Uploading {local_file} to {remote_path}...") - - success = self.device.upload(local_file, remote_path) - if success: - self.print_panel("SUCCESS", f"File uploaded to {remote_path}") - else: - self.print_panel("ERROR", "Upload failed!", color="red") diff --git a/ghost/modules/wifi.py b/ghost/modules/wifi.py deleted file mode 100644 index c6dcbf3e..00000000 --- a/ghost/modules/wifi.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -This module requires Ghost: https://github.com/EntySec/Ghost -Current source: https://github.com/EntySec/Ghost -""" - -from badges.cmd import Command -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() - - -class ExternalCommand(Command): - def __init__(self): - super().__init__({ - 'Category': "settings", - 'Name': "wifi", - 'Authors': [ - 'Ivan Nikolskiy (enty8080) - module developer' - ], - 'Description': "Set device wifi service state.", - 'Usage': "wifi ", - 'MinArgs': 1, - 'NeedsRoot': False - }) - - def print_panel(self, title: str, message: str, color: str = _PURPLE): - panel = Panel.fit( - Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color - ) - _console.print(panel) - - def run(self, args): - state = args[1].lower() - if state in ['on', 'off']: - action = "enable" if state == 'on' else "disable" - self.print_panel("PROCESS", f"Turning WiFi {state}...") - self.device.send_command(f"svc wifi {action}") - self.print_panel("SUCCESS", f"WiFi turned {state} successfully!") - else: - self.print_panel("USAGE", f"Usage: {self.info['Usage']}", color="red") From 7c0ac5136a6558d5adc95957f3d42fabeca49828 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 16:38:27 -0400 Subject: [PATCH 22/47] Remove setup.py --- setup.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 02da2725..00000000 --- a/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -from pathlib import Path -from setuptools import setup, find_packages - -HERE = Path(__file__).parent -readme = (HERE / "README.md").read_text(encoding="utf-8") if (HERE / "README.md").exists() else "" - -setup( - name="ghost", - version="8.0.0", - description="Ghost Framework — Android post-exploitation framework (ADB-based).", - long_description=readme, - long_description_content_type="text/markdown", - url="https://github.com/EntySec/Ghost", - author="EntySec", - author_email="entysec@gmail.com", - license="MIT", - python_requires=">=3.7", - packages=find_packages(exclude=("tests", "docs", "examples")), - include_package_data=True, - zip_safe=False, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Topic :: Security", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - ], - install_requires=[ - "adb-shell", - ], - entry_points={ - "console_scripts": [ - "ghost = ghost:cli", - ] - }, - project_urls={ - "Source": "https://github.com/EntySec/Ghost", - "Issues": "https://github.com/EntySec/Ghost/issues", - }, -) From 6fa71c791b010e1c5ed2e5c1d9cde670dd457de0 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 17:48:57 -0400 Subject: [PATCH 23/47] Remove install.py | Add main.py | Add pyproject.toml --- install.py => main.py | 78 +++++++++++++++++-------------------------- pyproject.toml | 28 ++++++++++++++++ 2 files changed, 58 insertions(+), 48 deletions(-) rename install.py => main.py (71%) create mode 100644 pyproject.toml diff --git a/install.py b/main.py similarity index 71% rename from install.py rename to main.py index 640c9c6e..0cc4f0e0 100755 --- a/install.py +++ b/main.py @@ -1,17 +1,4 @@ -#!/usr/bin/env python3 -""" -install.py — Ghost installer (full) - -- Interactive choice: virtualenv / --brek / system -- Automatically installs Ghost itself -- Installs all GitHub dependencies even in --brek/system mode -- Adds --break-system-packages for system Python -- Uses rich for terminal output -""" - from __future__ import annotations - -import argparse import os import subprocess import sys @@ -22,6 +9,7 @@ from rich.panel import Panel from rich.prompt import Prompt from rich.text import Text +import argparse console = Console() ROOT = Path(__file__).parent.resolve() @@ -33,18 +21,14 @@ "adb-shell" ] -PY_PACKAGES: List[str] = [] - DEFAULT_VENV = ROOT / ".venv" - def run(cmd: List[str], env: dict | None = None, check: bool = True) -> None: console.log(f"[bold purple]$[/bold purple] {' '.join(cmd)}") res = subprocess.run(cmd, env=env) if check and res.returncode != 0: raise SystemExit(f"Command failed: {' '.join(cmd)} (exit {res.returncode})") - def ensure_virtualenv(venv_path: Path) -> Tuple[str, str]: if venv_path.exists(): console.print(Panel(f"Using existing virtualenv: [bold]{venv_path}[/bold]", title="Virtualenv")) @@ -59,7 +43,6 @@ def ensure_virtualenv(venv_path: Path) -> Tuple[str, str]: pip = venv_path / "bin" / "pip" return str(py), str(pip) - def install_packages(py_exe: str, packages: List[str], break_system: bool = False) -> None: if not packages: return @@ -68,19 +51,17 @@ def install_packages(py_exe: str, packages: List[str], break_system: bool = Fals cmd.append("--break-system-packages") run(cmd) - def install_local_package(py_exe: str, break_system: bool = False) -> None: cmd = [py_exe, "-m", "pip", "install", ".", "--no-deps"] if break_system: cmd.append("--break-system-packages") run(cmd) - def main() -> None: - parser = argparse.ArgumentParser(description="Ghost installer (venv or --brek/system)") + parser = argparse.ArgumentParser(description="Ghost installer (venv or system)") parser.add_argument("--no-venv", action="store_true", help="Do not create/use virtualenv; install in current interpreter") parser.add_argument("--venv", default=".venv", help="Virtualenv path (default: .venv)") - parser.add_argument("--brek", action="store_true", help="Install only python-tool packages (filter out VCS/URLs).") + parser.add_argument("--brek", action="store_true", help="Install only python packages (skip VCS URLs).") parser.add_argument("--yes", "-y", action="store_true", help="Automatic yes for prompts") args = parser.parse_args() @@ -89,30 +70,34 @@ def main() -> None: use_brek = args.brek use_venv = not args.no_venv - if not (args.brek or args.no_venv): - choice = Prompt.ask( - "Choose install mode", - choices=["venv", "brek", "system"], - default="venv", - show_choices=True, - ) - if choice == "venv": - use_venv = True - use_brek = False - elif choice == "brek": - use_venv = False - use_brek = True - else: + # انتخاب خودکار در حالت بدون prompt + if not args.yes: + if not (args.brek or args.no_venv): + choice = Prompt.ask( + "Choose install mode", + choices=["venv", "brek", "system"], + default="venv", + show_choices=True, + ) + if choice == "venv": + use_venv = True + use_brek = False + elif choice == "brek": + use_venv = False + use_brek = True + else: + use_venv = False + use_brek = False + else: + if use_brek: use_venv = False - use_brek = False - break_system_flag = False - if not use_venv: - break_system_flag = True - console.print(Panel("Installing into current Python interpreter (no virtualenv). --break-system-packages enabled", title="Notice", style="yellow")) + break_system_flag = not use_venv + if break_system_flag: + console.print(Panel("Installing into current Python interpreter. --break-system-packages enabled", title="Notice", style="yellow")) if use_venv: - py_exe, pip_exe = ensure_virtualenv(Path(args.venv)) + py_exe, _ = ensure_virtualenv(Path(args.venv)) else: py_exe = sys.executable @@ -122,15 +107,12 @@ def main() -> None: cmd_upgrade.append("--break-system-packages") run(cmd_upgrade) - console.print(Panel(f"Installing VCS/GitHub packages ({len(VCS_PACKAGES)} items)...", title="Dependencies")) - install_packages(py_exe, VCS_PACKAGES, break_system_flag) + if not use_brek: + console.print(Panel(f"Installing VCS/GitHub packages ({len(VCS_PACKAGES)} items)...", title="Dependencies")) + install_packages(py_exe, VCS_PACKAGES, break_system_flag) console.print(Panel("Installing Ghost main package...", style="purple")) install_local_package(py_exe, break_system_flag) console.print(Panel("[bold green]Installation completed successfully![/bold green]")) console.print("You can now run: ghost") - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..caa2a3fd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=65", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ghost" +version = "1.0.0" +description = "Ghost — staged installation framework" +authors = [ + { name="Your Name", email="you@example.com" } +] +readme = "README.md" +requires-python = ">=3.9" + +dependencies = [ + "adb-shell" +] + +optional-dependencies = { + vcs = [ + "badges @ git+https://github.com/EntySec/Badges", + "pex @ git+https://github.com/EntySec/Pex", + "colorscript @ git+https://github.com/EntySec/ColorScript" + ] +} + +[project.scripts] +ghost = "ghost.main:main" From b455950fa851e4612f1d064d307d0c58f630e2e2 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 17:51:27 -0400 Subject: [PATCH 24/47] Revert "Remove setup.py" This reverts commit 573020a0c63327205117530b8e870b5def431f4e. --- ghost/__init__.py | 35 +++++ ghost/core/__init__.py | 23 +++ ghost/core/console.py | 299 ++++++++++++++++++++++++++++++++++++ ghost/core/device.py | 247 +++++++++++++++++++++++++++++ ghost/modules/activity.py | 50 ++++++ ghost/modules/battery.py | 51 ++++++ ghost/modules/download.py | 64 ++++++++ ghost/modules/keyboard.py | 57 +++++++ ghost/modules/list.py | 60 ++++++++ ghost/modules/network.py | 98 ++++++++++++ ghost/modules/openurl.py | 45 ++++++ ghost/modules/press.py | 45 ++++++ ghost/modules/screenshot.py | 51 ++++++ ghost/modules/shell.py | 42 +++++ ghost/modules/sleep.py | 41 +++++ ghost/modules/upload.py | 47 ++++++ ghost/modules/wifi.py | 46 ++++++ 17 files changed, 1301 insertions(+) create mode 100644 ghost/__init__.py create mode 100644 ghost/core/__init__.py create mode 100644 ghost/core/console.py create mode 100644 ghost/core/device.py create mode 100644 ghost/modules/activity.py create mode 100644 ghost/modules/battery.py create mode 100644 ghost/modules/download.py create mode 100644 ghost/modules/keyboard.py create mode 100644 ghost/modules/list.py create mode 100644 ghost/modules/network.py create mode 100644 ghost/modules/openurl.py create mode 100644 ghost/modules/press.py create mode 100644 ghost/modules/screenshot.py create mode 100644 ghost/modules/shell.py create mode 100644 ghost/modules/sleep.py create mode 100644 ghost/modules/upload.py create mode 100644 ghost/modules/wifi.py diff --git a/ghost/__init__.py b/ghost/__init__.py new file mode 100644 index 00000000..5cc61709 --- /dev/null +++ b/ghost/__init__.py @@ -0,0 +1,35 @@ +""" +MIT License + +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from ghost.core.console import Console + + +def cli() -> None: + """ Ghost Framework command-line interface. + + :return None: None + """ + + console = Console() + console.shell() diff --git a/ghost/core/__init__.py b/ghost/core/__init__.py new file mode 100644 index 00000000..e1ed48b4 --- /dev/null +++ b/ghost/core/__init__.py @@ -0,0 +1,23 @@ +""" +MIT License + +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" diff --git a/ghost/core/console.py b/ghost/core/console.py new file mode 100644 index 00000000..b8ba6fc4 --- /dev/null +++ b/ghost/core/console.py @@ -0,0 +1,299 @@ +""" +MIT License + +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from badges.cmd import Cmd + +from ghost.core.device import Device + +# --- Rich UI imports for styling (visual-only; logic untouched) --- +from rich.console import Console as RichConsole +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align +from rich import box +from rich.style import Style +from rich.rule import Rule +from rich.padding import Padding +from rich.columns import Columns +from rich.markdown import Markdown + +# Theme colors +PURPLE = "#7B61FF" +WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) +INFO_STYLE = Style(color=PURPLE, bold=True) +WARN_STYLE = Style(color="yellow", bold=True) +ERR_STYLE = Style(color="red", bold=True) +SUCCESS_STYLE = Style(color="green", bold=True) + + +class Console(Cmd): + """ Subclass of ghost.core module. + + This subclass of ghost.core modules is intended for providing + main Ghost Framework console interface. + """ + + def __init__(self) -> None: + # keep original prompt/intro tokens so framework behavior is unchanged + super().__init__( + prompt='(%lineghost%end)> ', + intro="""%clear%end + .--. .-. .-. + : .--': : .' `. + : : _ : `-. .--. .--.`. .' + : :; :: .. :' .; :`._-.': : + `.__.':_;:_;`.__.'`.__.':_; + +--=[ %bold%whiteGhost Framework 8.0.0%end +--=[ Developed by EntySec (%linehttps://entysec.com/%end) +""" + ) + + # preserve devices dict & logic + self.devices = {} + + # create rich console for all visual outputs + self.rich = RichConsole() + self._render_header() + + # ------------------------- + # Visual helpers (only) + # ------------------------- + def _render_header(self) -> None: + """Render a fancy hacker-style header with tools table and quick help.""" + title = Text("Ghost Framework 8.0.0", style="bold white") + subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") + + ascii_art = Text( + " .--. .-. .-.\n" + " : .--': : .' `.\n" + " : : _ : `-. .--. .--.`. .'\n" + " : :; :: .. :' .; :`._-.': :\n" + " `.__.':_;:_;`.__.'`.__.':_;", + justify="center", + ) + + left = Panel( + Align.center(ascii_art), + border_style=PURPLE, + box=box.HEAVY, + padding=(0, 2), + title="[bold]GHOST", + subtitle=title, + ) + + # Quick Help / Shortcuts table + quick_help = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + quick_help.add_column("Command / Alias", style="bold white", no_wrap=True) + quick_help.add_column("Description", style="dim") + + quick_help.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") + quick_help.add_row("devices", "List connected devices") + quick_help.add_row("disconnect ", "Disconnect device by ID") + quick_help.add_row("interact ", "Interact with a connected device") + quick_help.add_row("analyze ", "Run Device Analyzer") + quick_help.add_row("an ", "Alias for analyze") + quick_help.add_row("logcat ", "Start live logcat stream") + quick_help.add_row("lc ", "Alias for logcat") + quick_help.add_row("exit", "Quit Ghost Framework") + quick_help.add_row("Index 99", "Return to Menu / Exit (UI helper)") + + right_panel = Panel( + Align.left( + Text.assemble(subtitle, "\n\n", "Theme: ", (PURPLE, "Hacker • Purple")) + ), + border_style=PURPLE, + box=box.ROUNDED, + padding=(0, 1), + title="[bold]Info", + ) + + header_columns = Columns([left, Panel(Padding(quick_help, (1, 2)), border_style=PURPLE), right_panel]) + self.rich.print(header_columns) + self.rich.print(Rule(style=PURPLE)) + self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) + self.rich.print() + + + def print_empty(self, message: str = "") -> None: + # preserve signature and behavior, but print a small spacer + self.rich.print("") # purely visual + + def print_information(self, message: str) -> None: + # visually enhanced info box (preserves semantics) + self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) + + def print_warning(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) + + def print_error(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) + + def print_success(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) + + def print_usage(self, usage: str) -> None: + # show usage in an emphasized box + usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) + + def print_process(self, message: str) -> None: + # lightweight status indicator (visual only) + with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): + # no blocking logic here — just visual affordance + pass + + def print_table(self, title: str, columns: tuple, *rows) -> None: + """Render a stylized table for lists like connected devices.""" + table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + for col in columns: + table.add_column(str(col), header_style="bold white") + for row in rows: + # ensure each item is string (matching original behavior) + table.add_row(*[str(x) for x in row]) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) + self.rich.print(wrapper) + + # ------------------------- + # Original command logic (UNCHANGED) + # ------------------------- + def do_exit(self, _) -> None: + """ Exit Ghost Framework. + + :return None: None + :raises EOFError: EOF error + """ + + for device in list(self.devices): + self.devices[device]['device'].disconnect() + del self.devices[device] + + raise EOFError + + def do_connect(self, args: list) -> None: + """ Connect device. + + :param list args: arguments + :return None: None + """ + + if len(args) < 2: + self.print_usage("connect :[port]") + return + + address = args[1].split(':') + + if len(address) < 2: + host, port = address[0], 5555 + else: + host, port = address[0], int(address[1]) + + device = Device(host=host, port=port) + + if device.connect(): + self.devices.update({ + len(self.devices): { + 'host': host, + 'port': str(port), + 'device': device + } + }) + self.print_empty("") + + self.print_information( + f"Type %greendevices%end to list all connected devices.") + self.print_information( + f"Type %greeninteract {str(len(self.devices) - 1)}%end " + "to interact this device." + ) + + def do_devices(self, _) -> None: + """ Show connected devices. + + :return None: None + """ + + if not self.devices: + self.print_warning("No devices connected.") + return + + devices = [] + + for device in self.devices: + devices.append( + (device, self.devices[device]['host'], + self.devices[device]['port'])) + + self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) + + def do_disconnect(self, args: list) -> None: + """ Disconnect device. + + :param list args: arguments + :return None: None + """ + + if len(args) < 2: + self.print_usage("disconnect ") + return + + device_id = int(args[1]) + + if device_id not in self.devices: + self.print_error("Invalid device ID!") + return + + self.devices[device_id]['device'].disconnect() + self.devices.pop(device_id) + + def do_interact(self, args: list) -> None: + """ Interact with device. + + :param list args: arguments + :return None: None + """ + + if len(args) < 2: + self.print_usage("interact ") + return + + device_id = int(args[1]) + + if device_id not in self.devices: + self.print_error("Invalid device ID!") + return + + self.print_process(f"Interacting with device {str(device_id)}...") + self.devices[device_id]['device'].interact() + + def shell(self) -> None: + """ Run console shell. + + :return None: None + """ + + self.loop() diff --git a/ghost/core/device.py b/ghost/core/device.py new file mode 100644 index 00000000..d8d364d4 --- /dev/null +++ b/ghost/core/device.py @@ -0,0 +1,247 @@ +""" +MIT License + +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import os +import time +import threading +from badges.cmd import Cmd +from adb_shell.adb_device import AdbDeviceTcp +from adb_shell.auth.keygen import keygen +from adb_shell.auth.sign_pythonrsa import PythonRSASigner +from pex.fs import FS + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + +_MAX_RETRIES = 5 +_RETRY_DELAY = 3 +_HEALTHCHECK_DELAY = 10 + + +class Device(Cmd, FS): + """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" + + def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: + self.host = host + self.port = int(port) + self.key_file = key_filename + self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) + + self._reconnect_thread = None + self._stop_reconnect = threading.Event() + + super().__init__( + prompt=f'(%lineghost%end: %red{self.host}%end)> ', + path=[f'{os.path.dirname(os.path.dirname(__file__))}/modules'], + device=self + ) + + # ------------------------- + # Rich Print Helpers + # ------------------------- + def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: + _console.print(Panel.fit(Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color)) + + def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) + def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") + def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") + def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) + def print_empty(self): _console.print() + + # ------------------------- + # Device Keys + # ------------------------- + def get_keys(self) -> tuple: + if not os.path.exists(self.key_file): + keygen(self.key_file) + with open(self.key_file, 'r') as f: + priv = f.read() + with open(self.key_file + '.pub', 'r') as f: + pub = f.read() + return pub, priv + + # ------------------------- + # Connection Management + # ------------------------- + def connect(self, auto_reconnect: bool = True) -> bool: + self._stop_reconnect.clear() + self.print_process(f"Connecting to {self.host}...") + keys = self.get_keys() + signer = PythonRSASigner(*keys) + + for attempt in range(1, _MAX_RETRIES + 1): + try: + self.device.connect(rsa_keys=[signer], auth_timeout_s=5) + self.print_success(f"Connected to {self.host}!") + + if auto_reconnect: + self._start_auto_reconnect_thread() + return True + except Exception as e: + self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") + if attempt < _MAX_RETRIES: + self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") + time.sleep(_RETRY_DELAY) + + self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") + return False + + def _start_auto_reconnect_thread(self) -> None: + if self._reconnect_thread and self._reconnect_thread.is_alive(): return + self._reconnect_thread = threading.Thread(target=self.auto_reconnect, daemon=True) + self._reconnect_thread.start() + + def auto_reconnect(self) -> None: + while not self._stop_reconnect.is_set(): + try: + self.device.shell("echo ping", transport_timeout_s=3) + except Exception: + self.print_error(f"Lost connection to {self.host}, attempting reconnect...") + if not self.connect(auto_reconnect=False): + self.print_error("Auto-reconnect failed, will retry again...") + else: + self.print_success("Reconnected successfully!") + time.sleep(_HEALTHCHECK_DELAY) + + def disconnect(self) -> None: + self._stop_reconnect.set() + if self._reconnect_thread and self._reconnect_thread.is_alive(): + self._reconnect_thread.join(timeout=2) + try: + self.device.close() + self.print_success(f"Disconnected from {self.host}.") + except Exception: + self.print_error("Failed to disconnect properly!") + + # ------------------------- + # Command / File Operations + # ------------------------- + def send_command(self, command: str, output: bool = True) -> str: + try: + cmd_output = self.device.shell(command) + except Exception: + self.print_error("Socket is not connected!") + return None + return cmd_output if output else "" + + def list(self, path: str) -> list: + try: + return self.device.list(path) + except Exception: + self.print_error("Failed to list directory!") + return [] + + def download(self, input_file: str, output_path: str) -> bool: + exists, is_dir = self.exists(output_path) + if exists: + if is_dir: output_path += '/' + os.path.split(input_file)[1] + try: + self.print_process(f"Downloading {input_file}...") + self.device.pull(input_file, output_path) + self.print_success(f"Saved to {output_path}!") + return True + except Exception: + self.print_error(f"Remote file {input_file} not found or invalid!") + return False + + def upload(self, input_file: str, output_path: str) -> bool: + if self.check_file(input_file): + try: + self.print_process(f"Uploading {input_file}...") + self.device.push(input_file, output_path) + self.print_success(f"Saved to {output_path}!") + return True + except Exception: + try: + output_path += '/' + os.path.split(input_file)[1] + self.device.push(input_file, output_path) + except Exception: + self.print_error(f"Remote directory {output_path} does not exist!") + return False + + def is_rooted(self) -> bool: + responder = self.send_command('which su') + return bool(responder and not responder.isspace()) + + def interact(self) -> None: + self.print_success("Interactive connection spawned!") + self.print_process("Loading device modules...") + self.print_information(f"Modules loaded: {str(len(self.external))}") + self.loop() + + # ------------------------- + # Device Analyzer + # ------------------------- + def analyze_device(self): + self.print_process(f"Analyzing {self.host} ...") + try: + props = { + "Manufacturer": self.send_command("getprop ro.product.manufacturer"), + "Model": self.send_command("getprop ro.product.model"), + "Android Version": self.send_command("getprop ro.build.version.release"), + "Security Patch": self.send_command("getprop ro.build.version.security_patch"), + "Architecture": self.send_command("getprop ro.product.cpu.abi"), + "Rooted": "Yes" if self.is_rooted() else "No" + } + + table = Table(title=f"📱 Device Analysis — {self.host}", border_style=_PURPLE) + table.add_column("Property", style="bold white") + table.add_column("Value", style="dim") + + for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") + _console.print(table) + self.print_success("Analysis complete!") + except Exception as e: + self.print_error(f"Analysis failed: {e}") + + # ------------------------- + # Real-Time Log Viewer + # ------------------------- + def live_logcat(self): + self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") + + def stream_logs(): + try: + shell = self.device.shell("logcat -v time", decode=False) + for line in shell: + decoded = line.decode(errors="ignore").strip() + if " E " in decoded: _console.print(Text(decoded, style="red")) + elif " W " in decoded: _console.print(Text(decoded, style="yellow")) + else: _console.print(Text(decoded, style="dim")) + except KeyboardInterrupt: + self.print_process("Log streaming stopped by user.") + except Exception as e: + self.print_error(f"Logcat error: {e}") + + t = threading.Thread(target=stream_logs) + t.daemon = True + t.start() diff --git a/ghost/modules/activity.py b/ghost/modules/activity.py new file mode 100644 index 00000000..71c24139 --- /dev/null +++ b/ghost/modules/activity.py @@ -0,0 +1,50 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "settings", + 'Name': "activity", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Show device activity information.", + 'Usage': "activity", + 'MinArgs': 0, + 'NeedsRoot': False + }) + + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def run(self, _): + self.print_process("Getting activity information...") + + output = self.device.send_command("dumpsys activity") + self.print_empty(output) \ No newline at end of file diff --git a/ghost/modules/battery.py b/ghost/modules/battery.py new file mode 100644 index 00000000..289e6a43 --- /dev/null +++ b/ghost/modules/battery.py @@ -0,0 +1,51 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "settings", + 'Name': "battery", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Show device battery information.", + 'Usage': "battery", + 'MinArgs': 0, + 'NeedsRoot': False + }) + + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def run(self, _): + self.print_process("Getting battery information...") + + output = self.device.send_command("dumpsys battery") + self.print_empty(output) diff --git a/ghost/modules/download.py b/ghost/modules/download.py new file mode 100644 index 00000000..faa0f971 --- /dev/null +++ b/ghost/modules/download.py @@ -0,0 +1,64 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +import os +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "download", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Download file from device.", + 'Usage': "download ", + 'MinArgs': 2, + 'NeedsRoot': False + }) + + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_success(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("SUCCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_error(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("ERROR", style="bold white on " + _PURPLE), + border_style="red" + ) + _console.print(panel) + + def run(self, args): + remote_file, local_path = args[1], args[2] + self.print_process(f"Downloading {remote_file}...") + + success = self.device.download(remote_file, local_path) + if success: + self.print_success(f"File saved to {local_path}") + else: + self.print_error("Download failed!") diff --git a/ghost/modules/keyboard.py b/ghost/modules/keyboard.py new file mode 100644 index 00000000..ea9804e5 --- /dev/null +++ b/ghost/modules/keyboard.py @@ -0,0 +1,57 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +import sys +import termios +import tty +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "keyboard", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Interact with device keyboard.", + 'Usage': "keyboard", + 'MinArgs': 0, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + @staticmethod + def get_char(): + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + return sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + def run(self, _): + self.print_panel("PROCESS", "Interacting with keyboard...") + self.print_panel("SUCCESS", "Interactive connection spawned!") + self.print_panel("INFO", "Type text below.", color="cyan") + + while True: + self.device.send_command(f"input text {self.get_char()}") diff --git a/ghost/modules/list.py b/ghost/modules/list.py new file mode 100644 index 00000000..fa612b72 --- /dev/null +++ b/ghost/modules/list.py @@ -0,0 +1,60 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +import datetime +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "list", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "List directory contents.", + 'Usage': "list ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {_PURPLE}"), + border_style=_PURPLE + ) + _console.print(panel) + + def print_table_rich(self, title: str, headers: tuple, rows: list): + table = Table(title=title, header_style=f"bold {_PURPLE}", border_style=_PURPLE) + for h in headers: + table.add_column(h, style="white", no_wrap=True) + for row in rows: + table.add_row(*[str(i) for i in row]) + _console.print(table) + + def run(self, args): + self.print_panel("PROCESS", f"Listing directory: {args[1]}") + + output = self.device.list(args[1]) + if output: + headers = ('Name', 'Mode', 'Size', 'Modification Time') + rows = [] + for entry in sorted(output): + timestamp = datetime.datetime.fromtimestamp(entry[3]).strftime("%Y-%m-%d %H:%M:%S") + rows.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) + self.print_table_rich(f"Directory {args[1]}", headers, rows) + else: + self.print_panel("INFO", "No files found in this directory.") diff --git a/ghost/modules/network.py b/ghost/modules/network.py new file mode 100644 index 00000000..937711b7 --- /dev/null +++ b/ghost/modules/network.py @@ -0,0 +1,98 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "network", + 'Authors': [ + 'Loubaris - module developer' + ], + 'Description': "Retrieve network informations.", + 'NeedsRoot': False, + 'MinArgs': 1, + 'Options': [ + ( + ('-a', '--arp'), + {'help': "Show device ARP table.", 'action': 'store_true'} + ), + ( + ('-i', '--ipconfig'), + {'help': "Show device IP configuration.", 'action': 'store_true'} + ), + ( + ('-I', '--iproute'), + {'help': "Show device route table.", 'action': 'store_true'} + ), + ( + ('-l', '--locate'), + {'help': "Show device location.", 'action': 'store_true'} + ), + ( + ('-s', '--stats'), + {'help': "Show device network stats.", 'action': 'store_true'} + ), + ( + ('-p', '--ports'), + {'help': "Show device open ports.", 'action': 'store_true'} + ), + ( + ('-S', '--services'), + {'help': "Show device services.", 'action': 'store_true'} + ), + ( + ('-f', '--forwarding'), + {'help': "Show device forwarding rules.", 'action': 'store_true'} + ) + ] + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + outputs = [] + + if args.arp: + outputs.append(self.device.send_command('cat /proc/net/arp')) + + if args.ipconfig: + outputs.append(self.device.send_command('ip addr show')) + + if args.iproute: + outputs.append(self.device.send_command('ip route show')) + + if args.locate: + outputs.append(self.device.send_command('dumpsys location')) + + if args.stats: + outputs.append(self.device.send_command('cat /proc/net/netstat')) + + if args.ports: + outputs.append(self.device.send_command('busybox netstat -an')) + + if args.services: + outputs.append(self.device.send_command('service list')) + + if args.forwarding: + outputs.append(self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) + + self.print_panel("NETWORK OUTPUT", '\n'.join(outputs)) diff --git a/ghost/modules/openurl.py b/ghost/modules/openurl.py new file mode 100644 index 00000000..69d774b7 --- /dev/null +++ b/ghost/modules/openurl.py @@ -0,0 +1,45 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "openurl", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Open URL on device.", + 'Usage': "openurl ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + url = args[1] + if not url.startswith(("http://", "https://")): + url = "http://" + url + + self.print_panel("PROCESS", f"Opening URL on device: {url}") + self.device.send_command(f'am start -a android.intent.action.VIEW -d "{url}"') + self.print_panel("SUCCESS", "URL opened successfully!") diff --git a/ghost/modules/press.py b/ghost/modules/press.py new file mode 100644 index 00000000..0dfbcd86 --- /dev/null +++ b/ghost/modules/press.py @@ -0,0 +1,45 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "press", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Press device button by keycode.", + 'Usage': "press ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + keycode = int(args[1]) + if keycode < 124: + self.print_panel("PROCESS", f"Pressing device button with keycode: {keycode}") + self.device.send_command(f"input keyevent {keycode}") + self.print_panel("SUCCESS", "Key event sent successfully!") + else: + self.print_panel("ERROR", "Invalid keycode!", color="red") diff --git a/ghost/modules/screenshot.py b/ghost/modules/screenshot.py new file mode 100644 index 00000000..7c39be3a --- /dev/null +++ b/ghost/modules/screenshot.py @@ -0,0 +1,51 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +import os +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "screenshot", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Take device screenshot.", + 'Usage': "screenshot ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + local_path = args[1] + self.print_panel("PROCESS", "Taking screenshot on device...") + + self.device.send_command("screencap /data/local/tmp/screenshot.png") + success = self.device.download('/data/local/tmp/screenshot.png', local_path) + + if success: + self.print_panel("SUCCESS", f"Screenshot saved to {local_path}") + else: + self.print_panel("ERROR", "Failed to download screenshot!", color="red") + + self.device.send_command("rm /data/local/tmp/screenshot.png") diff --git a/ghost/modules/shell.py b/ghost/modules/shell.py new file mode 100644 index 00000000..bad6bb45 --- /dev/null +++ b/ghost/modules/shell.py @@ -0,0 +1,42 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "shell", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Execute shell command on device.", + 'Usage': "shell ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + command = ' '.join(args[1:]) + self.print_panel("PROCESS", f"Executing shell command: {command}") + output = self.device.send_command(command) + self.print_panel("OUTPUT", output) diff --git a/ghost/modules/sleep.py b/ghost/modules/sleep.py new file mode 100644 index 00000000..9b8af222 --- /dev/null +++ b/ghost/modules/sleep.py @@ -0,0 +1,41 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "sleep", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Put device into sleep mode.", + 'Usage': "sleep", + 'MinArgs': 0, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, _): + self.print_panel("PROCESS", "Putting device into sleep mode...") + self.device.send_command("input keyevent 26") + self.print_panel("SUCCESS", "Device is now in sleep mode!") diff --git a/ghost/modules/upload.py b/ghost/modules/upload.py new file mode 100644 index 00000000..f28b9c0a --- /dev/null +++ b/ghost/modules/upload.py @@ -0,0 +1,47 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +import os +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "manage", + 'Name': "upload", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Upload file to device.", + 'Usage': "upload ", + 'MinArgs': 2, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + local_file, remote_path = args[1], args[2] + self.print_panel("PROCESS", f"Uploading {local_file} to {remote_path}...") + + success = self.device.upload(local_file, remote_path) + if success: + self.print_panel("SUCCESS", f"File uploaded to {remote_path}") + else: + self.print_panel("ERROR", "Upload failed!", color="red") diff --git a/ghost/modules/wifi.py b/ghost/modules/wifi.py new file mode 100644 index 00000000..c6dcbf3e --- /dev/null +++ b/ghost/modules/wifi.py @@ -0,0 +1,46 @@ +""" +This module requires Ghost: https://github.com/EntySec/Ghost +Current source: https://github.com/EntySec/Ghost +""" + +from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() + + +class ExternalCommand(Command): + def __init__(self): + super().__init__({ + 'Category': "settings", + 'Name': "wifi", + 'Authors': [ + 'Ivan Nikolskiy (enty8080) - module developer' + ], + 'Description': "Set device wifi service state.", + 'Usage': "wifi ", + 'MinArgs': 1, + 'NeedsRoot': False + }) + + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + + def run(self, args): + state = args[1].lower() + if state in ['on', 'off']: + action = "enable" if state == 'on' else "disable" + self.print_panel("PROCESS", f"Turning WiFi {state}...") + self.device.send_command(f"svc wifi {action}") + self.print_panel("SUCCESS", f"WiFi turned {state} successfully!") + else: + self.print_panel("USAGE", f"Usage: {self.info['Usage']}", color="red") From 1b20270110c94d468f7c52e9f9513feaed46deeb Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 17:52:57 -0400 Subject: [PATCH 25/47] Big update --- README.md | 19 +++++++++++++++++++ main.py => ghost/main.py | 0 2 files changed, 19 insertions(+) rename main.py => ghost/main.py (100%) diff --git a/README.md b/README.md index 1128116d..ba647200 100755 --- a/README.md +++ b/README.md @@ -31,3 +31,22 @@
pip3 install git+https://github.com/EntySec/Ghost

+ +# Ghost + +Ghost — staged installation framework + +## install + +```bash + +python -m venv .venv +source .venv/bin/activate + + +pip install --upgrade pip +pip install .[vcs] + + +ghost + diff --git a/main.py b/ghost/main.py similarity index 100% rename from main.py rename to ghost/main.py From f50301b61e6a8d8cd31939603a8765b1efc56ef2 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 17:56:05 -0400 Subject: [PATCH 26/47] Update pyproject.toml --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index caa2a3fd..27afff5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,14 @@ name = "ghost" version = "1.0.0" description = "Ghost — staged installation framework" authors = [ - { name="Your Name", email="you@example.com" } + { name="enty8080 ", email="enty8080@gmail.com" } ] readme = "README.md" requires-python = ">=3.9" dependencies = [ - "adb-shell" + "adb-shell", + "rich" ] optional-dependencies = { From 0eb33d0af992bc1d0250e19b7ad2ae80dba4d5ad Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:01:18 -0400 Subject: [PATCH 27/47] Update pyproject.toml --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27afff5f..35fcc2e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,9 @@ dependencies = [ optional-dependencies = { vcs = [ - "badges @ git+https://github.com/EntySec/Badges", - "pex @ git+https://github.com/EntySec/Pex", - "colorscript @ git+https://github.com/EntySec/ColorScript" + "badges@git+https://github.com/EntySec/Badges", + "pex@git+https://github.com/EntySec/Pex", + "colorscript@git+https://github.com/EntySec/ColorScript" ] } From 3385696a3594fffdeac1d9d257c84dabe030080d Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:03:57 -0400 Subject: [PATCH 28/47] last Update pyproject.toml --- pyproject.toml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35fcc2e1..fce4c232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,24 +6,18 @@ build-backend = "setuptools.build_meta" name = "ghost" version = "1.0.0" description = "Ghost — staged installation framework" -authors = [ - { name="enty8080 ", email="enty8080@gmail.com" } -] +authors = [{ name="Your Name", email="you@example.com" }] readme = "README.md" requires-python = ">=3.9" +dependencies = ["adb-shell", "rich"] -dependencies = [ - "adb-shell", - "rich" +[project.optional-dependencies] +vcs = [ + "badges@git+https://github.com/EntySec/Badges", + "pex@git+https://github.com/EntySec/Pex", + "colorscript@git+https://github.com/EntySec/ColorScript" ] -optional-dependencies = { - vcs = [ - "badges@git+https://github.com/EntySec/Badges", - "pex@git+https://github.com/EntySec/Pex", - "colorscript@git+https://github.com/EntySec/ColorScript" - ] -} [project.scripts] ghost = "ghost.main:main" From a393e0276642185b247cbdf9c1bd28db5a161ed1 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:06:15 -0400 Subject: [PATCH 29/47] Remoce some comments --- ghost/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ghost/main.py b/ghost/main.py index 0cc4f0e0..5fcb669e 100755 --- a/ghost/main.py +++ b/ghost/main.py @@ -70,7 +70,6 @@ def main() -> None: use_brek = args.brek use_venv = not args.no_venv - # انتخاب خودکار در حالت بدون prompt if not args.yes: if not (args.brek or args.no_venv): choice = Prompt.ask( From 6b23ddefa2c2a1de7b084bca1857af7c4ad99328 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:07:03 -0400 Subject: [PATCH 30/47] Remoce some comments --- ghost/core/console.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index b8ba6fc4..7cd21cdd 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -26,7 +26,6 @@ from ghost.core.device import Device -# --- Rich UI imports for styling (visual-only; logic untouched) --- from rich.console import Console as RichConsole from rich.panel import Panel from rich.table import Table @@ -39,7 +38,6 @@ from rich.columns import Columns from rich.markdown import Markdown -# Theme colors PURPLE = "#7B61FF" WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) INFO_STYLE = Style(color=PURPLE, bold=True) @@ -56,7 +54,6 @@ class Console(Cmd): """ def __init__(self) -> None: - # keep original prompt/intro tokens so framework behavior is unchanged super().__init__( prompt='(%lineghost%end)> ', intro="""%clear%end @@ -71,16 +68,12 @@ def __init__(self) -> None: """ ) - # preserve devices dict & logic self.devices = {} - # create rich console for all visual outputs self.rich = RichConsole() self._render_header() - # ------------------------- - # Visual helpers (only) - # ------------------------- + def _render_header(self) -> None: """Render a fancy hacker-style header with tools table and quick help.""" title = Text("Ghost Framework 8.0.0", style="bold white") @@ -104,7 +97,6 @@ def _render_header(self) -> None: subtitle=title, ) - # Quick Help / Shortcuts table quick_help = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) quick_help.add_column("Command / Alias", style="bold white", no_wrap=True) quick_help.add_column("Description", style="dim") @@ -138,11 +130,8 @@ def _render_header(self) -> None: def print_empty(self, message: str = "") -> None: - # preserve signature and behavior, but print a small spacer - self.rich.print("") # purely visual - + self.rich.print("") def print_information(self, message: str) -> None: - # visually enhanced info box (preserves semantics) self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) def print_warning(self, message: str) -> None: @@ -155,15 +144,12 @@ def print_success(self, message: str) -> None: self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) def print_usage(self, usage: str) -> None: - # show usage in an emphasized box usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) def print_process(self, message: str) -> None: - # lightweight status indicator (visual only) with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): - # no blocking logic here — just visual affordance pass def print_table(self, title: str, columns: tuple, *rows) -> None: @@ -172,15 +158,11 @@ def print_table(self, title: str, columns: tuple, *rows) -> None: for col in columns: table.add_column(str(col), header_style="bold white") for row in rows: - # ensure each item is string (matching original behavior) table.add_row(*[str(x) for x in row]) footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) self.rich.print(wrapper) - # ------------------------- - # Original command logic (UNCHANGED) - # ------------------------- def do_exit(self, _) -> None: """ Exit Ghost Framework. From c2ad2224d2105bc42558c8842014086111e9c6e2 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:07:38 -0400 Subject: [PATCH 31/47] Remoce some comments --- ghost/core/device.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/ghost/core/device.py b/ghost/core/device.py index d8d364d4..b03f1617 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -63,9 +63,6 @@ def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: device=self ) - # ------------------------- - # Rich Print Helpers - # ------------------------- def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: _console.print(Panel.fit(Align.left(Text(message)), title=Text(title, style=f"bold white on {color}"), @@ -77,9 +74,7 @@ def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) def print_empty(self): _console.print() - # ------------------------- - # Device Keys - # ------------------------- + def get_keys(self) -> tuple: if not os.path.exists(self.key_file): keygen(self.key_file) @@ -89,9 +84,7 @@ def get_keys(self) -> tuple: pub = f.read() return pub, priv - # ------------------------- - # Connection Management - # ------------------------- + def connect(self, auto_reconnect: bool = True) -> bool: self._stop_reconnect.clear() self.print_process(f"Connecting to {self.host}...") @@ -142,9 +135,7 @@ def disconnect(self) -> None: except Exception: self.print_error("Failed to disconnect properly!") - # ------------------------- - # Command / File Operations - # ------------------------- + def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) @@ -198,9 +189,6 @@ def interact(self) -> None: self.print_information(f"Modules loaded: {str(len(self.external))}") self.loop() - # ------------------------- - # Device Analyzer - # ------------------------- def analyze_device(self): self.print_process(f"Analyzing {self.host} ...") try: @@ -223,9 +211,6 @@ def analyze_device(self): except Exception as e: self.print_error(f"Analysis failed: {e}") - # ------------------------- - # Real-Time Log Viewer - # ------------------------- def live_logcat(self): self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") From 38e083b314c75a324e210b0828f38a4d3e832daf Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:13:21 -0400 Subject: [PATCH 32/47] Remove main.py | update pyproject.toml --- ghost/main.py | 117 ------------------------------------------------- pyproject.toml | 9 ++-- 2 files changed, 5 insertions(+), 121 deletions(-) delete mode 100755 ghost/main.py diff --git a/ghost/main.py b/ghost/main.py deleted file mode 100755 index 5fcb669e..00000000 --- a/ghost/main.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations -import os -import subprocess -import sys -from pathlib import Path -from typing import List, Tuple - -from rich.console import Console -from rich.panel import Panel -from rich.prompt import Prompt -from rich.text import Text -import argparse - -console = Console() -ROOT = Path(__file__).parent.resolve() - -VCS_PACKAGES = [ - "badges @ git+https://github.com/EntySec/Badges", - "pex @ git+https://github.com/EntySec/Pex", - "colorscript @ git+https://github.com/EntySec/ColorScript", - "adb-shell" -] - -DEFAULT_VENV = ROOT / ".venv" - -def run(cmd: List[str], env: dict | None = None, check: bool = True) -> None: - console.log(f"[bold purple]$[/bold purple] {' '.join(cmd)}") - res = subprocess.run(cmd, env=env) - if check and res.returncode != 0: - raise SystemExit(f"Command failed: {' '.join(cmd)} (exit {res.returncode})") - -def ensure_virtualenv(venv_path: Path) -> Tuple[str, str]: - if venv_path.exists(): - console.print(Panel(f"Using existing virtualenv: [bold]{venv_path}[/bold]", title="Virtualenv")) - else: - console.print(Panel(f"Creating virtualenv at: [bold]{venv_path}[/bold]", title="Virtualenv")) - run([sys.executable, "-m", "venv", str(venv_path)]) - if os.name == "nt": - py = venv_path / "Scripts" / "python.exe" - pip = venv_path / "Scripts" / "pip.exe" - else: - py = venv_path / "bin" / "python" - pip = venv_path / "bin" / "pip" - return str(py), str(pip) - -def install_packages(py_exe: str, packages: List[str], break_system: bool = False) -> None: - if not packages: - return - cmd = [py_exe, "-m", "pip", "install"] + packages - if break_system: - cmd.append("--break-system-packages") - run(cmd) - -def install_local_package(py_exe: str, break_system: bool = False) -> None: - cmd = [py_exe, "-m", "pip", "install", ".", "--no-deps"] - if break_system: - cmd.append("--break-system-packages") - run(cmd) - -def main() -> None: - parser = argparse.ArgumentParser(description="Ghost installer (venv or system)") - parser.add_argument("--no-venv", action="store_true", help="Do not create/use virtualenv; install in current interpreter") - parser.add_argument("--venv", default=".venv", help="Virtualenv path (default: .venv)") - parser.add_argument("--brek", action="store_true", help="Install only python packages (skip VCS URLs).") - parser.add_argument("--yes", "-y", action="store_true", help="Automatic yes for prompts") - args = parser.parse_args() - - console.print(Panel(Text("Ghost Installer — staged installation\n\nIndex 99 → Return to Menu / Exit", justify="center"), style="purple")) - - use_brek = args.brek - use_venv = not args.no_venv - - if not args.yes: - if not (args.brek or args.no_venv): - choice = Prompt.ask( - "Choose install mode", - choices=["venv", "brek", "system"], - default="venv", - show_choices=True, - ) - if choice == "venv": - use_venv = True - use_brek = False - elif choice == "brek": - use_venv = False - use_brek = True - else: - use_venv = False - use_brek = False - else: - if use_brek: - use_venv = False - - break_system_flag = not use_venv - if break_system_flag: - console.print(Panel("Installing into current Python interpreter. --break-system-packages enabled", title="Notice", style="yellow")) - - if use_venv: - py_exe, _ = ensure_virtualenv(Path(args.venv)) - else: - py_exe = sys.executable - - console.print("[bold]Upgrading pip in target environment...[/bold]") - cmd_upgrade = [py_exe, "-m", "pip", "install", "--upgrade", "pip"] - if break_system_flag: - cmd_upgrade.append("--break-system-packages") - run(cmd_upgrade) - - if not use_brek: - console.print(Panel(f"Installing VCS/GitHub packages ({len(VCS_PACKAGES)} items)...", title="Dependencies")) - install_packages(py_exe, VCS_PACKAGES, break_system_flag) - - console.print(Panel("Installing Ghost main package...", style="purple")) - install_local_package(py_exe, break_system_flag) - - console.print(Panel("[bold green]Installation completed successfully![/bold green]")) - console.print("You can now run: ghost") diff --git a/pyproject.toml b/pyproject.toml index fce4c232..b9bd854b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,13 @@ build-backend = "setuptools.build_meta" [project] name = "ghost" version = "1.0.0" -description = "Ghost — staged installation framework" -authors = [{ name="Your Name", email="you@example.com" }] +description = "Ghost CLI tool — no interactive installer" readme = "README.md" requires-python = ">=3.9" -dependencies = ["adb-shell", "rich"] +dependencies = [ + "adb-shell", + "rich" +] [project.optional-dependencies] vcs = [ @@ -18,6 +20,5 @@ vcs = [ "colorscript@git+https://github.com/EntySec/ColorScript" ] - [project.scripts] ghost = "ghost.main:main" From 97728268d1766359d4aae789646d5576753ac8e2 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:17:08 -0400 Subject: [PATCH 33/47] Add main.py --- ghost/main.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 ghost/main.py diff --git a/ghost/main.py b/ghost/main.py new file mode 100644 index 00000000..099fad6a --- /dev/null +++ b/ghost/main.py @@ -0,0 +1,13 @@ +from rich.console import Console +console = Console() + +def main(): + console.print("[bold purple]Ghost CLI — Ready![/bold purple]") + while True: + cmd = input("ghost> ") + if cmd in ("exit", "quit"): + break + console.print(f"You typed: {cmd}") + +if __name__ == "__main__": + main() From 5782752a2382b5e899a5351f5df68dfa418e3973 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:33:28 -0400 Subject: [PATCH 34/47] Big update 2 --- ghost/__init__.py | 10 +++------- ghost/main.py | 13 ------------- pyproject.toml | 15 ++++++--------- 3 files changed, 9 insertions(+), 29 deletions(-) delete mode 100644 ghost/main.py diff --git a/ghost/__init__.py b/ghost/__init__.py index 5cc61709..959bf79a 100644 --- a/ghost/__init__.py +++ b/ghost/__init__.py @@ -24,12 +24,8 @@ from ghost.core.console import Console - -def cli() -> None: - """ Ghost Framework command-line interface. - - :return None: None - """ - +def main(): + """Ghost CLI entry-point.""" console = Console() console.shell() + diff --git a/ghost/main.py b/ghost/main.py deleted file mode 100644 index 099fad6a..00000000 --- a/ghost/main.py +++ /dev/null @@ -1,13 +0,0 @@ -from rich.console import Console -console = Console() - -def main(): - console.print("[bold purple]Ghost CLI — Ready![/bold purple]") - while True: - cmd = input("ghost> ") - if cmd in ("exit", "quit"): - break - console.print(f"You typed: {cmd}") - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index b9bd854b..1804dfad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,17 @@ build-backend = "setuptools.build_meta" [project] name = "ghost" version = "1.0.0" -description = "Ghost CLI tool — no interactive installer" +description = "Ghost CLI tool" readme = "README.md" requires-python = ">=3.9" -dependencies = [ - "adb-shell", - "rich" -] +dependencies = ["rich", "adb-shell"] [project.optional-dependencies] vcs = [ - "badges@git+https://github.com/EntySec/Badges", - "pex@git+https://github.com/EntySec/Pex", - "colorscript@git+https://github.com/EntySec/ColorScript" + "badges @ git+https://github.com/EntySec/Badges", + "pex @ git+https://github.com/EntySec/Pex", + "colorscript @ git+https://github.com/EntySec/ColorScript" ] [project.scripts] -ghost = "ghost.main:main" +ghost = "ghost:main" From d578246c00e22295fa8902da16bad1c57b065808 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:36:02 -0400 Subject: [PATCH 35/47] update pyproject.toml --- pyproject.toml | 8 +++++++- requirements.txt | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1804dfad..a6f3a983 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,12 @@ version = "1.0.0" description = "Ghost CLI tool" readme = "README.md" requires-python = ">=3.9" -dependencies = ["rich", "adb-shell"] + +dependencies = [ + "adb-shell", + "rich<13.0.0" +] + [project.optional-dependencies] vcs = [ @@ -17,5 +22,6 @@ vcs = [ "colorscript @ git+https://github.com/EntySec/ColorScript" ] + [project.scripts] ghost = "ghost:main" diff --git a/requirements.txt b/requirements.txt index 2aa89705..cfb4ba15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ adb-shell>=0.1 pex @ git+https://github.com/EntySec/Pex.git badges @ git+https://github.com/EntySec/Badges.git colorscript @ git+https://github.com/EntySec/ColorScript.git -rich>=10.0 +rich>=13.0.0 + From 9e93c4bc5686ec086d33c30dfee806f3a193f201 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 18:38:52 -0400 Subject: [PATCH 36/47] update REAADME.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ba647200..3bb9f062 100755 --- a/README.md +++ b/README.md @@ -36,6 +36,14 @@ Ghost — staged installation framework +## fast-install + +```bash + +pip install .[vcs] --break + +``` + ## install ```bash @@ -50,3 +58,5 @@ pip install .[vcs] ghost +``` + From 180e94cf14507d01e420974be3e0fd64375b2611 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 19:01:50 -0400 Subject: [PATCH 37/47] last update --- ghost/core/console.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index 7cd21cdd..e72c7901 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -97,20 +97,30 @@ def _render_header(self) -> None: subtitle=title, ) - quick_help = Table(box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) - quick_help.add_column("Command / Alias", style="bold white", no_wrap=True) - quick_help.add_column("Description", style="dim") - - quick_help.add_row("connect :[port]", "Connect to device via ADB (default port 5555)") - quick_help.add_row("devices", "List connected devices") - quick_help.add_row("disconnect ", "Disconnect device by ID") - quick_help.add_row("interact ", "Interact with a connected device") - quick_help.add_row("analyze ", "Run Device Analyzer") - quick_help.add_row("an ", "Alias for analyze") - quick_help.add_row("logcat ", "Start live logcat stream") - quick_help.add_row("lc ", "Alias for logcat") - quick_help.add_row("exit", "Quit Ghost Framework") - quick_help.add_row("Index 99", "Return to Menu / Exit (UI helper)") + help_table = Table(title=Text("🚀 Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), + box=box.DOUBLE_EDGE, + border_style=PURPLE, + expand=False, + show_lines=True) + + help_table.add_column("Command", style="bold white on " + "#5A3EFF", no_wrap=True, justify="center") + help_table.add_column("Description", style="italic dim", justify="left") + + commands = [ + ("🔌 connect :[port]", "Connect to device via ADB (default port 5555)"), + ("📱 devices", "List connected devices"), + ("❌ disconnect ", "Disconnect device by ID"), + ("💬 interact ", "Interact with a connected device"), + ("🔍 analyze / an ", "Run Device Analyzer"), + ("📜 logcat / lc ", "Start live logcat stream"), + ("🚪 exit", "Quit Ghost Framework"), + ("🔄 Index 99", "Return to Menu / Exit (UI helper)") + ] + + ALT_ROW = "#2E2E2E" + for i, (cmd, desc) in enumerate(commands): + style = Style(bgcolor=ALT_ROW) if i % 2 else Style() + help_table.add_row(cmd, desc, style=style) right_panel = Panel( Align.left( @@ -122,13 +132,12 @@ def _render_header(self) -> None: title="[bold]Info", ) - header_columns = Columns([left, Panel(Padding(quick_help, (1, 2)), border_style=PURPLE), right_panel]) + header_columns = Columns([left, Panel(help_table, padding=(1, 2), border_style=PURPLE), right_panel]) self.rich.print(header_columns) self.rich.print(Rule(style=PURPLE)) self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) self.rich.print() - def print_empty(self, message: str = "") -> None: self.rich.print("") def print_information(self, message: str) -> None: From cf75102c94881bfe6c419518e57076d785cbf43b Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 19:45:03 -0400 Subject: [PATCH 38/47] Update pyproject.toml --- pyproject.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a6f3a983..d8389c3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,17 +11,11 @@ requires-python = ">=3.9" dependencies = [ "adb-shell", - "rich<13.0.0" -] - - -[project.optional-dependencies] -vcs = [ + "rich<13.0.0", "badges @ git+https://github.com/EntySec/Badges", "pex @ git+https://github.com/EntySec/Pex", "colorscript @ git+https://github.com/EntySec/ColorScript" ] - [project.scripts] ghost = "ghost:main" From 81b1d48493caf7b61a335c1fc7d6eab9ec86e751 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 19:51:29 -0400 Subject: [PATCH 39/47] Update console.py & device.py --- ghost/core/console.py | 170 ++++++++---------------------------------- ghost/core/device.py | 96 +++++++++--------------- 2 files changed, 67 insertions(+), 199 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index e72c7901..c87157b5 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -23,9 +23,7 @@ """ from badges.cmd import Cmd - from ghost.core.device import Device - from rich.console import Console as RichConsole from rich.panel import Panel from rich.table import Table @@ -36,22 +34,12 @@ from rich.rule import Rule from rich.padding import Padding from rich.columns import Columns -from rich.markdown import Markdown PURPLE = "#7B61FF" -WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) INFO_STYLE = Style(color=PURPLE, bold=True) -WARN_STYLE = Style(color="yellow", bold=True) -ERR_STYLE = Style(color="red", bold=True) -SUCCESS_STYLE = Style(color="green", bold=True) - class Console(Cmd): - """ Subclass of ghost.core module. - - This subclass of ghost.core modules is intended for providing - main Ghost Framework console interface. - """ + """ Subclass of ghost.core module using badges for printing. """ def __init__(self) -> None: super().__init__( @@ -69,13 +57,10 @@ def __init__(self) -> None: ) self.devices = {} - - self.rich = RichConsole() self._render_header() - def _render_header(self) -> None: - """Render a fancy hacker-style header with tools table and quick help.""" + """Render fancy header with modern Help table using Rich.""" title = Text("Ghost Framework 8.0.0", style="bold white") subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") @@ -97,13 +82,16 @@ def _render_header(self) -> None: subtitle=title, ) - help_table = Table(title=Text("🚀 Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), - box=box.DOUBLE_EDGE, - border_style=PURPLE, - expand=False, - show_lines=True) + # Help table + help_table = Table( + title=Text("🚀 Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), + box=box.DOUBLE_EDGE, + border_style=PURPLE, + expand=False, + show_lines=True + ) - help_table.add_column("Command", style="bold white on " + "#5A3EFF", no_wrap=True, justify="center") + help_table.add_column("Command", style="bold white on #5A3EFF", no_wrap=True, justify="center") help_table.add_column("Description", style="italic dim", justify="left") commands = [ @@ -133,158 +121,62 @@ def _render_header(self) -> None: ) header_columns = Columns([left, Panel(help_table, padding=(1, 2), border_style=PURPLE), right_panel]) - self.rich.print(header_columns) - self.rich.print(Rule(style=PURPLE)) - self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) - self.rich.print() - - def print_empty(self, message: str = "") -> None: - self.rich.print("") - def print_information(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) - - def print_warning(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) - - def print_error(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) - - def print_success(self, message: str) -> None: - self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) - - def print_usage(self, usage: str) -> None: - usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) - footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) - self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) - - def print_process(self, message: str) -> None: - with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): - pass - - def print_table(self, title: str, columns: tuple, *rows) -> None: - """Render a stylized table for lists like connected devices.""" - table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) - for col in columns: - table.add_column(str(col), header_style="bold white") - for row in rows: - table.add_row(*[str(x) for x in row]) - footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) - wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) - self.rich.print(wrapper) + self.print(header_columns) + self.print(Rule(style=PURPLE)) + self.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) + self.print() + # ===== Device commands ===== def do_exit(self, _) -> None: - """ Exit Ghost Framework. - - :return None: None - :raises EOFError: EOF error - """ - for device in list(self.devices): self.devices[device]['device'].disconnect() del self.devices[device] - raise EOFError def do_connect(self, args: list) -> None: - """ Connect device. - - :param list args: arguments - :return None: None - """ - if len(args) < 2: - self.print_usage("connect :[port]") + self.usage("connect :[port]") return address = args[1].split(':') - - if len(address) < 2: - host, port = address[0], 5555 - else: - host, port = address[0], int(address[1]) - + host, port = (address[0], 5555) if len(address) < 2 else (address[0], int(address[1])) device = Device(host=host, port=port) if device.connect(): - self.devices.update({ - len(self.devices): { - 'host': host, - 'port': str(port), - 'device': device - } - }) - self.print_empty("") - - self.print_information( - f"Type %greendevices%end to list all connected devices.") - self.print_information( - f"Type %greeninteract {str(len(self.devices) - 1)}%end " - "to interact this device." - ) + self.devices.update({len(self.devices): {'host': host, 'port': str(port), 'device': device}}) + self.info(f"Type %greendevices%end to list all connected devices.") + self.info(f"Type %greeninteract {len(self.devices) - 1}%end to interact with this device.") def do_devices(self, _) -> None: - """ Show connected devices. - - :return None: None - """ - if not self.devices: - self.print_warning("No devices connected.") + self.warn("No devices connected.") return - devices = [] - - for device in self.devices: - devices.append( - (device, self.devices[device]['host'], - self.devices[device]['port'])) - - self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) + devices = [(dev, self.devices[dev]['host'], self.devices[dev]['port']) for dev in self.devices] + self.table("Connected Devices", ("ID", "Host", "Port"), *devices) def do_disconnect(self, args: list) -> None: - """ Disconnect device. - - :param list args: arguments - :return None: None - """ - if len(args) < 2: - self.print_usage("disconnect ") + self.usage("disconnect ") return - device_id = int(args[1]) - if device_id not in self.devices: - self.print_error("Invalid device ID!") + self.error("Invalid device ID!") return - self.devices[device_id]['device'].disconnect() self.devices.pop(device_id) def do_interact(self, args: list) -> None: - """ Interact with device. - - :param list args: arguments - :return None: None - """ - if len(args) < 2: - self.print_usage("interact ") + self.usage("interact ") return - device_id = int(args[1]) - if device_id not in self.devices: - self.print_error("Invalid device ID!") + self.error("Invalid device ID!") return - - self.print_process(f"Interacting with device {str(device_id)}...") + self.process(f"Interacting with device {device_id}...") self.devices[device_id]['device'].interact() - - def shell(self) -> None: - """ Run console shell. - - :return None: None - """ + def shell(self) -> None: self.loop() + diff --git a/ghost/core/device.py b/ghost/core/device.py index b03f1617..752ead7a 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -31,29 +31,22 @@ from adb_shell.auth.sign_pythonrsa import PythonRSASigner from pex.fs import FS -from rich.console import Console -from rich.panel import Panel from rich.table import Table from rich.text import Text -from rich.align import Align - -_PURPLE = "#7B61FF" -_console = Console() _MAX_RETRIES = 5 _RETRY_DELAY = 3 _HEALTHCHECK_DELAY = 10 - +_PURPLE = "#7B61FF" class Device(Cmd, FS): - """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" + """Enhanced Device class fully compatible with badges.""" def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: self.host = host self.port = int(port) self.key_file = key_filename self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) - self._reconnect_thread = None self._stop_reconnect = threading.Event() @@ -63,18 +56,6 @@ def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: device=self ) - def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: - _console.print(Panel.fit(Align.left(Text(message)), - title=Text(title, style=f"bold white on {color}"), - border_style=color)) - - def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) - def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") - def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") - def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) - def print_empty(self): _console.print() - - def get_keys(self) -> tuple: if not os.path.exists(self.key_file): keygen(self.key_file) @@ -84,28 +65,26 @@ def get_keys(self) -> tuple: pub = f.read() return pub, priv - def connect(self, auto_reconnect: bool = True) -> bool: self._stop_reconnect.clear() - self.print_process(f"Connecting to {self.host}...") + self.process(f"Connecting to {self.host}...") keys = self.get_keys() signer = PythonRSASigner(*keys) for attempt in range(1, _MAX_RETRIES + 1): try: self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - self.print_success(f"Connected to {self.host}!") - + self.success(f"Connected to {self.host}!") if auto_reconnect: self._start_auto_reconnect_thread() return True except Exception as e: - self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") + self.error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") if attempt < _MAX_RETRIES: - self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") + self.info(f"Retrying in {_RETRY_DELAY} seconds...") time.sleep(_RETRY_DELAY) - self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") + self.error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") return False def _start_auto_reconnect_thread(self) -> None: @@ -118,11 +97,11 @@ def auto_reconnect(self) -> None: try: self.device.shell("echo ping", transport_timeout_s=3) except Exception: - self.print_error(f"Lost connection to {self.host}, attempting reconnect...") + self.error(f"Lost connection to {self.host}, attempting reconnect...") if not self.connect(auto_reconnect=False): - self.print_error("Auto-reconnect failed, will retry again...") + self.error("Auto-reconnect failed, will retry again...") else: - self.print_success("Reconnected successfully!") + self.success("Reconnected successfully!") time.sleep(_HEALTHCHECK_DELAY) def disconnect(self) -> None: @@ -131,16 +110,15 @@ def disconnect(self) -> None: self._reconnect_thread.join(timeout=2) try: self.device.close() - self.print_success(f"Disconnected from {self.host}.") + self.success(f"Disconnected from {self.host}.") except Exception: - self.print_error("Failed to disconnect properly!") - + self.error("Failed to disconnect properly!") def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) except Exception: - self.print_error("Socket is not connected!") + self.error("Socket is not connected!") return None return cmd_output if output else "" @@ -148,7 +126,7 @@ def list(self, path: str) -> list: try: return self.device.list(path) except Exception: - self.print_error("Failed to list directory!") + self.error("Failed to list directory!") return [] def download(self, input_file: str, output_path: str) -> bool: @@ -156,27 +134,27 @@ def download(self, input_file: str, output_path: str) -> bool: if exists: if is_dir: output_path += '/' + os.path.split(input_file)[1] try: - self.print_process(f"Downloading {input_file}...") + self.process(f"Downloading {input_file}...") self.device.pull(input_file, output_path) - self.print_success(f"Saved to {output_path}!") + self.success(f"Saved to {output_path}!") return True except Exception: - self.print_error(f"Remote file {input_file} not found or invalid!") + self.error(f"Remote file {input_file} not found or invalid!") return False def upload(self, input_file: str, output_path: str) -> bool: if self.check_file(input_file): try: - self.print_process(f"Uploading {input_file}...") + self.process(f"Uploading {input_file}...") self.device.push(input_file, output_path) - self.print_success(f"Saved to {output_path}!") + self.success(f"Saved to {output_path}!") return True except Exception: try: output_path += '/' + os.path.split(input_file)[1] self.device.push(input_file, output_path) except Exception: - self.print_error(f"Remote directory {output_path} does not exist!") + self.error(f"Remote directory {output_path} does not exist!") return False def is_rooted(self) -> bool: @@ -184,13 +162,13 @@ def is_rooted(self) -> bool: return bool(responder and not responder.isspace()) def interact(self) -> None: - self.print_success("Interactive connection spawned!") - self.print_process("Loading device modules...") - self.print_information(f"Modules loaded: {str(len(self.external))}") + self.success("Interactive connection spawned!") + self.process("Loading device modules...") + self.info(f"Modules loaded: {str(len(self.external))}") self.loop() def analyze_device(self): - self.print_process(f"Analyzing {self.host} ...") + self.process(f"Analyzing {self.host} ...") try: props = { "Manufacturer": self.send_command("getprop ro.product.manufacturer"), @@ -204,29 +182,27 @@ def analyze_device(self): table = Table(title=f"📱 Device Analysis — {self.host}", border_style=_PURPLE) table.add_column("Property", style="bold white") table.add_column("Value", style="dim") - - for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") - _console.print(table) - self.print_success("Analysis complete!") + for k, v in props.items(): + table.add_row(k, v.strip() if v else "N/A") + self.print(table) + self.success("Analysis complete!") except Exception as e: - self.print_error(f"Analysis failed: {e}") + self.error(f"Analysis failed: {e}") def live_logcat(self): - self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") - + self.info("Starting live logcat stream (Press Ctrl+C to stop)...") def stream_logs(): try: shell = self.device.shell("logcat -v time", decode=False) for line in shell: decoded = line.decode(errors="ignore").strip() - if " E " in decoded: _console.print(Text(decoded, style="red")) - elif " W " in decoded: _console.print(Text(decoded, style="yellow")) - else: _console.print(Text(decoded, style="dim")) + if " E " in decoded: self.print(Text(decoded, style="red")) + elif " W " in decoded: self.print(Text(decoded, style="yellow")) + else: self.print(Text(decoded, style="dim")) except KeyboardInterrupt: - self.print_process("Log streaming stopped by user.") + self.process("Log streaming stopped by user.") except Exception as e: - self.print_error(f"Logcat error: {e}") + self.error(f"Logcat error: {e}") - t = threading.Thread(target=stream_logs) - t.daemon = True + t = threading.Thread(target=stream_logs, daemon=True) t.start() From 6f06cbe592d6068bce035b6ac30960bb461fb0da Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 20:04:46 -0400 Subject: [PATCH 40/47] Update main to cli --- ghost/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/__init__.py b/ghost/__init__.py index 959bf79a..3221e2a0 100644 --- a/ghost/__init__.py +++ b/ghost/__init__.py @@ -24,7 +24,7 @@ from ghost.core.console import Console -def main(): +def cli(): """Ghost CLI entry-point.""" console = Console() console.shell() From 10400291f267e53fb4225b7df3c31b1d8687859d Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 20:05:46 -0400 Subject: [PATCH 41/47] Update main to cli --- ghost/core/console.py | 170 ++++++++++++++++++++++++++++++++++-------- ghost/core/device.py | 96 +++++++++++++++--------- pyproject.toml | 2 +- 3 files changed, 200 insertions(+), 68 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index c87157b5..e72c7901 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -23,7 +23,9 @@ """ from badges.cmd import Cmd + from ghost.core.device import Device + from rich.console import Console as RichConsole from rich.panel import Panel from rich.table import Table @@ -34,12 +36,22 @@ from rich.rule import Rule from rich.padding import Padding from rich.columns import Columns +from rich.markdown import Markdown PURPLE = "#7B61FF" +WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) INFO_STYLE = Style(color=PURPLE, bold=True) +WARN_STYLE = Style(color="yellow", bold=True) +ERR_STYLE = Style(color="red", bold=True) +SUCCESS_STYLE = Style(color="green", bold=True) + class Console(Cmd): - """ Subclass of ghost.core module using badges for printing. """ + """ Subclass of ghost.core module. + + This subclass of ghost.core modules is intended for providing + main Ghost Framework console interface. + """ def __init__(self) -> None: super().__init__( @@ -57,10 +69,13 @@ def __init__(self) -> None: ) self.devices = {} + + self.rich = RichConsole() self._render_header() + def _render_header(self) -> None: - """Render fancy header with modern Help table using Rich.""" + """Render a fancy hacker-style header with tools table and quick help.""" title = Text("Ghost Framework 8.0.0", style="bold white") subtitle = Text("Developed by EntySec — https://entysec.com/", style="dim") @@ -82,16 +97,13 @@ def _render_header(self) -> None: subtitle=title, ) - # Help table - help_table = Table( - title=Text("🚀 Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), - box=box.DOUBLE_EDGE, - border_style=PURPLE, - expand=False, - show_lines=True - ) + help_table = Table(title=Text("🚀 Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), + box=box.DOUBLE_EDGE, + border_style=PURPLE, + expand=False, + show_lines=True) - help_table.add_column("Command", style="bold white on #5A3EFF", no_wrap=True, justify="center") + help_table.add_column("Command", style="bold white on " + "#5A3EFF", no_wrap=True, justify="center") help_table.add_column("Description", style="italic dim", justify="left") commands = [ @@ -121,62 +133,158 @@ def _render_header(self) -> None: ) header_columns = Columns([left, Panel(help_table, padding=(1, 2), border_style=PURPLE), right_panel]) - self.print(header_columns) - self.print(Rule(style=PURPLE)) - self.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) - self.print() + self.rich.print(header_columns) + self.rich.print(Rule(style=PURPLE)) + self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) + self.rich.print() + + def print_empty(self, message: str = "") -> None: + self.rich.print("") + def print_information(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) + + def print_warning(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) + + def print_error(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) + + def print_success(self, message: str) -> None: + self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) + + def print_usage(self, usage: str) -> None: + usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) + + def print_process(self, message: str) -> None: + with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): + pass + + def print_table(self, title: str, columns: tuple, *rows) -> None: + """Render a stylized table for lists like connected devices.""" + table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + for col in columns: + table.add_column(str(col), header_style="bold white") + for row in rows: + table.add_row(*[str(x) for x in row]) + footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) + wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) + self.rich.print(wrapper) - # ===== Device commands ===== def do_exit(self, _) -> None: + """ Exit Ghost Framework. + + :return None: None + :raises EOFError: EOF error + """ + for device in list(self.devices): self.devices[device]['device'].disconnect() del self.devices[device] + raise EOFError def do_connect(self, args: list) -> None: + """ Connect device. + + :param list args: arguments + :return None: None + """ + if len(args) < 2: - self.usage("connect :[port]") + self.print_usage("connect :[port]") return address = args[1].split(':') - host, port = (address[0], 5555) if len(address) < 2 else (address[0], int(address[1])) + + if len(address) < 2: + host, port = address[0], 5555 + else: + host, port = address[0], int(address[1]) + device = Device(host=host, port=port) if device.connect(): - self.devices.update({len(self.devices): {'host': host, 'port': str(port), 'device': device}}) - self.info(f"Type %greendevices%end to list all connected devices.") - self.info(f"Type %greeninteract {len(self.devices) - 1}%end to interact with this device.") + self.devices.update({ + len(self.devices): { + 'host': host, + 'port': str(port), + 'device': device + } + }) + self.print_empty("") + + self.print_information( + f"Type %greendevices%end to list all connected devices.") + self.print_information( + f"Type %greeninteract {str(len(self.devices) - 1)}%end " + "to interact this device." + ) def do_devices(self, _) -> None: + """ Show connected devices. + + :return None: None + """ + if not self.devices: - self.warn("No devices connected.") + self.print_warning("No devices connected.") return - devices = [(dev, self.devices[dev]['host'], self.devices[dev]['port']) for dev in self.devices] - self.table("Connected Devices", ("ID", "Host", "Port"), *devices) + devices = [] + + for device in self.devices: + devices.append( + (device, self.devices[device]['host'], + self.devices[device]['port'])) + + self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) def do_disconnect(self, args: list) -> None: + """ Disconnect device. + + :param list args: arguments + :return None: None + """ + if len(args) < 2: - self.usage("disconnect ") + self.print_usage("disconnect ") return + device_id = int(args[1]) + if device_id not in self.devices: - self.error("Invalid device ID!") + self.print_error("Invalid device ID!") return + self.devices[device_id]['device'].disconnect() self.devices.pop(device_id) def do_interact(self, args: list) -> None: + """ Interact with device. + + :param list args: arguments + :return None: None + """ + if len(args) < 2: - self.usage("interact ") + self.print_usage("interact ") return + device_id = int(args[1]) + if device_id not in self.devices: - self.error("Invalid device ID!") + self.print_error("Invalid device ID!") return - self.process(f"Interacting with device {device_id}...") - self.devices[device_id]['device'].interact() + self.print_process(f"Interacting with device {str(device_id)}...") + self.devices[device_id]['device'].interact() + def shell(self) -> None: - self.loop() + """ Run console shell. + :return None: None + """ + + self.loop() diff --git a/ghost/core/device.py b/ghost/core/device.py index 752ead7a..b03f1617 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -31,22 +31,29 @@ from adb_shell.auth.sign_pythonrsa import PythonRSASigner from pex.fs import FS +from rich.console import Console +from rich.panel import Panel from rich.table import Table from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() _MAX_RETRIES = 5 _RETRY_DELAY = 3 _HEALTHCHECK_DELAY = 10 -_PURPLE = "#7B61FF" + class Device(Cmd, FS): - """Enhanced Device class fully compatible with badges.""" + """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: self.host = host self.port = int(port) self.key_file = key_filename self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) + self._reconnect_thread = None self._stop_reconnect = threading.Event() @@ -56,6 +63,18 @@ def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: device=self ) + def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: + _console.print(Panel.fit(Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color)) + + def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) + def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") + def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") + def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) + def print_empty(self): _console.print() + + def get_keys(self) -> tuple: if not os.path.exists(self.key_file): keygen(self.key_file) @@ -65,26 +84,28 @@ def get_keys(self) -> tuple: pub = f.read() return pub, priv + def connect(self, auto_reconnect: bool = True) -> bool: self._stop_reconnect.clear() - self.process(f"Connecting to {self.host}...") + self.print_process(f"Connecting to {self.host}...") keys = self.get_keys() signer = PythonRSASigner(*keys) for attempt in range(1, _MAX_RETRIES + 1): try: self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - self.success(f"Connected to {self.host}!") + self.print_success(f"Connected to {self.host}!") + if auto_reconnect: self._start_auto_reconnect_thread() return True except Exception as e: - self.error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") + self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") if attempt < _MAX_RETRIES: - self.info(f"Retrying in {_RETRY_DELAY} seconds...") + self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") time.sleep(_RETRY_DELAY) - self.error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") + self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") return False def _start_auto_reconnect_thread(self) -> None: @@ -97,11 +118,11 @@ def auto_reconnect(self) -> None: try: self.device.shell("echo ping", transport_timeout_s=3) except Exception: - self.error(f"Lost connection to {self.host}, attempting reconnect...") + self.print_error(f"Lost connection to {self.host}, attempting reconnect...") if not self.connect(auto_reconnect=False): - self.error("Auto-reconnect failed, will retry again...") + self.print_error("Auto-reconnect failed, will retry again...") else: - self.success("Reconnected successfully!") + self.print_success("Reconnected successfully!") time.sleep(_HEALTHCHECK_DELAY) def disconnect(self) -> None: @@ -110,15 +131,16 @@ def disconnect(self) -> None: self._reconnect_thread.join(timeout=2) try: self.device.close() - self.success(f"Disconnected from {self.host}.") + self.print_success(f"Disconnected from {self.host}.") except Exception: - self.error("Failed to disconnect properly!") + self.print_error("Failed to disconnect properly!") + def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) except Exception: - self.error("Socket is not connected!") + self.print_error("Socket is not connected!") return None return cmd_output if output else "" @@ -126,7 +148,7 @@ def list(self, path: str) -> list: try: return self.device.list(path) except Exception: - self.error("Failed to list directory!") + self.print_error("Failed to list directory!") return [] def download(self, input_file: str, output_path: str) -> bool: @@ -134,27 +156,27 @@ def download(self, input_file: str, output_path: str) -> bool: if exists: if is_dir: output_path += '/' + os.path.split(input_file)[1] try: - self.process(f"Downloading {input_file}...") + self.print_process(f"Downloading {input_file}...") self.device.pull(input_file, output_path) - self.success(f"Saved to {output_path}!") + self.print_success(f"Saved to {output_path}!") return True except Exception: - self.error(f"Remote file {input_file} not found or invalid!") + self.print_error(f"Remote file {input_file} not found or invalid!") return False def upload(self, input_file: str, output_path: str) -> bool: if self.check_file(input_file): try: - self.process(f"Uploading {input_file}...") + self.print_process(f"Uploading {input_file}...") self.device.push(input_file, output_path) - self.success(f"Saved to {output_path}!") + self.print_success(f"Saved to {output_path}!") return True except Exception: try: output_path += '/' + os.path.split(input_file)[1] self.device.push(input_file, output_path) except Exception: - self.error(f"Remote directory {output_path} does not exist!") + self.print_error(f"Remote directory {output_path} does not exist!") return False def is_rooted(self) -> bool: @@ -162,13 +184,13 @@ def is_rooted(self) -> bool: return bool(responder and not responder.isspace()) def interact(self) -> None: - self.success("Interactive connection spawned!") - self.process("Loading device modules...") - self.info(f"Modules loaded: {str(len(self.external))}") + self.print_success("Interactive connection spawned!") + self.print_process("Loading device modules...") + self.print_information(f"Modules loaded: {str(len(self.external))}") self.loop() def analyze_device(self): - self.process(f"Analyzing {self.host} ...") + self.print_process(f"Analyzing {self.host} ...") try: props = { "Manufacturer": self.send_command("getprop ro.product.manufacturer"), @@ -182,27 +204,29 @@ def analyze_device(self): table = Table(title=f"📱 Device Analysis — {self.host}", border_style=_PURPLE) table.add_column("Property", style="bold white") table.add_column("Value", style="dim") - for k, v in props.items(): - table.add_row(k, v.strip() if v else "N/A") - self.print(table) - self.success("Analysis complete!") + + for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") + _console.print(table) + self.print_success("Analysis complete!") except Exception as e: - self.error(f"Analysis failed: {e}") + self.print_error(f"Analysis failed: {e}") def live_logcat(self): - self.info("Starting live logcat stream (Press Ctrl+C to stop)...") + self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") + def stream_logs(): try: shell = self.device.shell("logcat -v time", decode=False) for line in shell: decoded = line.decode(errors="ignore").strip() - if " E " in decoded: self.print(Text(decoded, style="red")) - elif " W " in decoded: self.print(Text(decoded, style="yellow")) - else: self.print(Text(decoded, style="dim")) + if " E " in decoded: _console.print(Text(decoded, style="red")) + elif " W " in decoded: _console.print(Text(decoded, style="yellow")) + else: _console.print(Text(decoded, style="dim")) except KeyboardInterrupt: - self.process("Log streaming stopped by user.") + self.print_process("Log streaming stopped by user.") except Exception as e: - self.error(f"Logcat error: {e}") + self.print_error(f"Logcat error: {e}") - t = threading.Thread(target=stream_logs, daemon=True) + t = threading.Thread(target=stream_logs) + t.daemon = True t.start() diff --git a/pyproject.toml b/pyproject.toml index d8389c3a..41d06993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,4 +18,4 @@ dependencies = [ ] [project.scripts] -ghost = "ghost:main" +ghost = "ghost:cli" From 32d2ebeed94ff412c2a023ea1d8211a54874588a Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 20:15:56 -0400 Subject: [PATCH 42/47] Remove requirements.txt & update pyproject.toml --- pyproject.toml | 30 +++++++++++++++++++++--------- requirements.txt | 6 ------ 2 files changed, 21 insertions(+), 15 deletions(-) delete mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 41d06993..922623c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,33 @@ [build-system] -requires = ["setuptools>=65", "wheel"] +requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" [project] name = "ghost" -version = "1.0.0" -description = "Ghost CLI tool" +version = "8.0.0" +description = "Ghost Framework is an Android post-exploitation framework that exploits the Android Debug Bridge to remotely access an Android device." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.7.0" +license = { file = "LICENSE" } +authors = [ + { name = "EntySec", email = "entysec@gmail.com" } +] +urls = { Homepage = "https://github.com/EntySec/Ghost" } dependencies = [ - "adb-shell", - "rich<13.0.0", - "badges @ git+https://github.com/EntySec/Badges", - "pex @ git+https://github.com/EntySec/Pex", - "colorscript @ git+https://github.com/EntySec/ColorScript" + "adb-shell", + "pex @ git+https://github.com/EntySec/Pex", + "badges @ git+https://github.com/EntySec/Badges", + "colorscript @ git+https://github.com/EntySec/ColorScript", ] [project.scripts] ghost = "ghost:cli" + +[tool.setuptools.packages.find] +where = ["."] +include = ["*"] +exclude = [] + +[tool.setuptools] +include-package-data = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cfb4ba15..00000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -adb-shell>=0.1 -pex @ git+https://github.com/EntySec/Pex.git -badges @ git+https://github.com/EntySec/Badges.git -colorscript @ git+https://github.com/EntySec/ColorScript.git -rich>=13.0.0 - From d602f792a058e492afac810a7c44bfef590a0b77 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 20:21:20 -0400 Subject: [PATCH 43/47] update console.py --- ghost/core/console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index e72c7901..1a8132e6 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -138,8 +138,8 @@ def _render_header(self) -> None: self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices — Index 99 → Exit", style=INFO_STYLE))) self.rich.print() - def print_empty(self, message: str = "") -> None: - self.rich.print("") + def print_empty(self, message: str = "", end: str = "\n") -> None: + self.rich.print(message) def print_information(self, message: str) -> None: self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) From cb8b58102610d8d53e08221cad8d2e9a16e0ec9d Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 20:34:08 -0400 Subject: [PATCH 44/47] Add __main__.py --- ghost/__init__.py | 30 ------------------------------ ghost/__main__.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 ghost/__main__.py diff --git a/ghost/__init__.py b/ghost/__init__.py index 3221e2a0..d3f5a12f 100644 --- a/ghost/__init__.py +++ b/ghost/__init__.py @@ -1,31 +1 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from ghost.core.console import Console - -def cli(): - """Ghost CLI entry-point.""" - console = Console() - console.shell() diff --git a/ghost/__main__.py b/ghost/__main__.py new file mode 100644 index 00000000..6bbf5895 --- /dev/null +++ b/ghost/__main__.py @@ -0,0 +1,37 @@ +""" +MIT License + +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from ghost.core.console import Console + +def cli() -> None: + """ Ghost Framework command-line interface. + + :return None: None + """ + + console = Console() + console.shell() + +if __name__ == "__main__": + cli() \ No newline at end of file From 0802ca5ffdf37a009c6f81d595c72156c909314a Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 21:06:06 -0400 Subject: [PATCH 45/47] last update --- ghost/__init__.py | 24 ++++++++++++++++++++++++ ghost/__main__.py | 37 ++----------------------------------- ghost/cli.py | 5 +++++ 3 files changed, 31 insertions(+), 35 deletions(-) create mode 100644 ghost/cli.py diff --git a/ghost/__init__.py b/ghost/__init__.py index d3f5a12f..fcc907ff 100644 --- a/ghost/__init__.py +++ b/ghost/__init__.py @@ -1 +1,25 @@ +""" +MIT License +Copyright (c) 2020-2024 EntySec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +__version__ = "…" \ No newline at end of file diff --git a/ghost/__main__.py b/ghost/__main__.py index 6bbf5895..0ef3024d 100644 --- a/ghost/__main__.py +++ b/ghost/__main__.py @@ -1,37 +1,4 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from ghost.core.console import Console - -def cli() -> None: - """ Ghost Framework command-line interface. - - :return None: None - """ - - console = Console() - console.shell() +from .cli import main if __name__ == "__main__": - cli() \ No newline at end of file + raise SystemExit(main()) \ No newline at end of file diff --git a/ghost/cli.py b/ghost/cli.py new file mode 100644 index 00000000..d5bf9ef7 --- /dev/null +++ b/ghost/cli.py @@ -0,0 +1,5 @@ +from ghost.core.console import Console + +def main() -> int: + Console().shell() + return 0 \ No newline at end of file From 566c371b3af8df4c51c3306d96ac5e310b738f01 Mon Sep 17 00:00:00 2001 From: Modark Date: Fri, 17 Oct 2025 21:06:23 -0400 Subject: [PATCH 46/47] last update --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 922623c2..3b673e40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ ] [project.scripts] -ghost = "ghost:cli" +ghost = "ghost.cli:main" [tool.setuptools.packages.find] where = ["."] From fa39dbf581a298f070cd92e729fd58c20303359f Mon Sep 17 00:00:00 2001 From: Modark Date: Sat, 18 Oct 2025 06:01:30 -0400 Subject: [PATCH 47/47] update interfaace --- ghost/core/console.py | 70 +++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/ghost/core/console.py b/ghost/core/console.py index 1a8132e6..da7de45e 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -23,7 +23,6 @@ """ from badges.cmd import Cmd - from ghost.core.device import Device from rich.console import Console as RichConsole @@ -36,7 +35,6 @@ from rich.rule import Rule from rich.padding import Padding from rich.columns import Columns -from rich.markdown import Markdown PURPLE = "#7B61FF" WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) @@ -56,21 +54,23 @@ class Console(Cmd): def __init__(self) -> None: super().__init__( prompt='(%lineghost%end)> ', - intro="""%clear%end + intro=""" .--. .-. .-. : .--': : .' `. : : _ : `-. .--. .--.`. .' : :; :: .. :' .; :`._-.': : `.__.':_;:_;`.__.'`.__.':_; ---=[ %bold%whiteGhost Framework 8.0.0%end ---=[ Developed by EntySec (%linehttps://entysec.com/%end) +--=[ Ghost Framework 8.0.0 +--=[ Developed by EntySec (https://entysec.com/) """ ) self.devices = {} - self.rich = RichConsole() + # Force ANSI + TrueColor for Linux/modern terminals + self.rich = RichConsole(force_terminal=True, color_system="truecolor") + self.rich.clear() self._render_header() @@ -113,6 +113,7 @@ def _render_header(self) -> None: ("💬 interact ", "Interact with a connected device"), ("🔍 analyze / an ", "Run Device Analyzer"), ("📜 logcat / lc ", "Start live logcat stream"), + ("🧹 clear", "Clear the terminal screen"), ("🚪 exit", "Quit Ghost Framework"), ("🔄 Index 99", "Return to Menu / Exit (UI helper)") ] @@ -139,25 +140,32 @@ def _render_header(self) -> None: self.rich.print() def print_empty(self, message: str = "", end: str = "\n") -> None: + """Print a simple message.""" self.rich.print(message) def print_information(self, message: str) -> None: + """Print an informational message in a panel.""" self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) def print_warning(self, message: str) -> None: + """Print a warning message in a panel.""" self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) def print_error(self, message: str) -> None: + """Print an error message in a panel.""" self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) def print_success(self, message: str) -> None: + """Print a success message in a panel.""" self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) def print_usage(self, usage: str) -> None: + """Print usage information for a command.""" usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) footer = Text("Index 99 → Return to Menu", style=INFO_STYLE) self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) def print_process(self, message: str) -> None: + """Show a processing spinner with a message.""" with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): pass @@ -173,25 +181,14 @@ def print_table(self, title: str, columns: tuple, *rows) -> None: self.rich.print(wrapper) def do_exit(self, _) -> None: - """ Exit Ghost Framework. - - :return None: None - :raises EOFError: EOF error - """ - + """Quit Ghost Framework and disconnect all devices.""" for device in list(self.devices): self.devices[device]['device'].disconnect() del self.devices[device] - raise EOFError def do_connect(self, args: list) -> None: - """ Connect device. - - :param list args: arguments - :return None: None - """ - + """Connect to a device via ADB.""" if len(args) < 2: self.print_usage("connect :[port]") return @@ -214,7 +211,6 @@ def do_connect(self, args: list) -> None: } }) self.print_empty("") - self.print_information( f"Type %greendevices%end to list all connected devices.") self.print_information( @@ -223,37 +219,25 @@ def do_connect(self, args: list) -> None: ) def do_devices(self, _) -> None: - """ Show connected devices. - - :return None: None - """ - + """List all connected devices.""" if not self.devices: self.print_warning("No devices connected.") return devices = [] - for device in self.devices: devices.append( (device, self.devices[device]['host'], self.devices[device]['port'])) - self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) def do_disconnect(self, args: list) -> None: - """ Disconnect device. - - :param list args: arguments - :return None: None - """ - + """Disconnect a connected device by ID.""" if len(args) < 2: self.print_usage("disconnect ") return device_id = int(args[1]) - if device_id not in self.devices: self.print_error("Invalid device ID!") return @@ -262,29 +246,23 @@ def do_disconnect(self, args: list) -> None: self.devices.pop(device_id) def do_interact(self, args: list) -> None: - """ Interact with device. - - :param list args: arguments - :return None: None - """ - + """Interact with a connected device by ID.""" if len(args) < 2: self.print_usage("interact ") return device_id = int(args[1]) - if device_id not in self.devices: self.print_error("Invalid device ID!") return self.print_process(f"Interacting with device {str(device_id)}...") self.devices[device_id]['device'].interact() - - def shell(self) -> None: - """ Run console shell. - :return None: None - """ + def do_clear(self, _) -> None: + """Clear the terminal screen.""" + self.rich.clear() + def shell(self) -> None: + """Start the main Ghost Framework loop.""" self.loop()