diff --git a/README.md b/README.md
index 1128116d..3bb9f062 100755
--- a/README.md
+++ b/README.md
@@ -31,3 +31,32 @@
pip3 install git+https://github.com/EntySec/Ghost
+
+# Ghost
+
+Ghost โ staged installation framework
+
+## fast-install
+
+```bash
+
+pip install .[vcs] --break
+
+```
+
+## install
+
+```bash
+
+python -m venv .venv
+source .venv/bin/activate
+
+
+pip install --upgrade pip
+pip install .[vcs]
+
+
+ghost
+
+```
+
diff --git a/ghost/__init__.py b/ghost/__init__.py
index 5cc61709..fcc907ff 100644
--- a/ghost/__init__.py
+++ b/ghost/__init__.py
@@ -22,14 +22,4 @@
SOFTWARE.
"""
-from ghost.core.console import Console
-
-
-def cli() -> None:
- """ Ghost Framework command-line interface.
-
- :return None: None
- """
-
- console = Console()
- console.shell()
+__version__ = "โฆ"
\ No newline at end of file
diff --git a/ghost/__main__.py b/ghost/__main__.py
new file mode 100644
index 00000000..0ef3024d
--- /dev/null
+++ b/ghost/__main__.py
@@ -0,0 +1,4 @@
+from .cli import main
+
+if __name__ == "__main__":
+ 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
diff --git a/ghost/core/console.py b/ghost/core/console.py
index 1abded23..da7de45e 100644
--- a/ghost/core/console.py
+++ b/ghost/core/console.py
@@ -23,9 +23,26 @@
"""
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
+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
+
+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.
@@ -37,40 +54,141 @@ 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 = {}
- def do_exit(self, _) -> None:
- """ Exit Ghost Framework.
+ # Force ANSI + TrueColor for Linux/modern terminals
+ self.rich = RichConsole(force_terminal=True, color_system="truecolor")
+ self.rich.clear()
+ self._render_header()
+
- :return None: None
- :raises EOFError: EOF error
- """
+ 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,
+ )
+ 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"),
+ ("๐งน clear", "Clear the terminal screen"),
+ ("๐ช 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(
+ 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(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 = "", 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
+
+ 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)
+
+ def do_exit(self, _) -> None:
+ """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
@@ -93,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(
@@ -102,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
@@ -141,18 +246,12 @@ 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
@@ -160,10 +259,10 @@ 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.
-
- :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()
diff --git a/ghost/core/device.py b/ghost/core/device.py
index 16fa2201..b03f1617 100644
--- a/ghost/core/device.py
+++ b/ghost/core/device.py
@@ -23,203 +23,210 @@
"""
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
-class Device(Cmd, FS):
- """ Subclass of ghost.core module.
+_PURPLE = "#7B61FF"
+_console = Console()
- This subclass of ghost.core module is intended for providing
- an implementation of device controller.
- """
+_MAX_RETRIES = 5
+_RETRY_DELAY = 3
+_HEALTHCHECK_DELAY = 10
- 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, 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
)
- def get_keys(self) -> tuple:
- """ Get cryptographic keys.
+ 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))
- :return tuple: public key, private key
- """
+ 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)
+ 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()
- with open(self.key_file + '.pub', 'r') as file:
- pub = file.read()
- return pub, priv
+ 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)
- def send_command(self, command: str, output: bool = True) -> str:
- """ Send command to the device.
+ 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
- :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:
+ 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!")
+
+
+ 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]
-
+ 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_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]
+ 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()
+
+ 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}")
+
+ 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
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...")
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!")
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()}")
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.")
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))
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!")
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")
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")
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)
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!")
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")
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")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..3b673e40
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,33 @@
+[build-system]
+requires = ["setuptools>=61", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+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."
+readme = "README.md"
+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",
+ "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:main"
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["*"]
+exclude = []
+
+[tool.setuptools]
+include-package-data = true
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 59dad0c1..00000000
--- a/setup.py
+++ /dev/null
@@ -1,52 +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 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
- )
diff --git a/uninstall.py b/uninstall.py
new file mode 100644
index 00000000..fb5319b8
--- /dev/null
+++ b/uninstall.py
@@ -0,0 +1,83 @@
+#!/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()
+
+PYTHON_PACKAGES = [
+ "ghost",
+ "adb-shell",
+ "pex",
+ "badges",
+ "colorscript",
+]
+
+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)
+
+ 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()