Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
include requirements.txt
18 changes: 11 additions & 7 deletions objection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@

def _load_version() -> str:
"""
Prefer the installed package metadata and fall back to pyproject.toml
when running from a checkout.
Read the checkout version when running from source, otherwise use the
installed package metadata.
"""

try:
return metadata.version("objection")
except metadata.PackageNotFoundError:
pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
if pyproject_path.exists():
try:
with pyproject_path.open("rb") as f:
return tomllib.load(f)["project"]["version"]
except Exception:
return "0.0.0"

pass

try:
return metadata.version("objection")
except metadata.PackageNotFoundError:
return "0.0.0"


__version__ = _load_version()
Expand Down
23 changes: 23 additions & 0 deletions objection/commands/command_history.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import datetime
import os

import click

from ..state.app import app_state


def format_history_timestamp(timestamp: str) -> str:
"""Format a prompt-toolkit timestamp for compact history output."""

try:
return datetime.datetime.fromisoformat(timestamp).strftime('%Y-%m-%d %H:%M')
except (TypeError, ValueError):
return timestamp


def numbered_history(commands: list, timestamps: list = None) -> None:
"""Print a numbered history suitable for selecting or replaying entries."""

click.secho('Historic commands:', dim=True)

for number, command in enumerate(commands, start=1):
timestamp = ''
if timestamps and number <= len(timestamps) and timestamps[number - 1]:
timestamp = '{0} '.format(format_history_timestamp(timestamps[number - 1]))

click.secho('{0} {1}{2}'.format(number, timestamp, command))


def history(args: list) -> None:
"""
Lists the commands that have been run in the current session.
Expand Down
13 changes: 9 additions & 4 deletions objection/console/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,10 @@ def api():
help='A script to import and run before the repl polls the device for information.')
@click.option('--enable-api', '-a', required=False, default=False, is_flag=True,
help='Start the objection API server.')
@click.option('--history-limit', '-H', required=False, type=click.IntRange(min=0), default=0, show_default=True,
help='Number of latest history entries to show at startup. Disabled by default.')
def start(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File,
enable_api: bool) -> None:
enable_api: bool, history_limit: int) -> None:
"""
Start a new session
"""
Expand Down Expand Up @@ -200,7 +202,7 @@ def api_thread():
time.sleep(2)

# drop into the repl
repl.run(quiet=quiet)
repl.run(quiet=quiet, history_limit=history_limit)

# Some ugly backwards compatibility
@cli.command(deprecated="Use 'objection start' instead of 'objection explore'", hidden=True)
Expand All @@ -215,8 +217,10 @@ def api_thread():
help='A script to import and run before the repl polls the device for information.')
@click.option('--enable-api', '-a', required=False, default=False, is_flag=True,
help='Start the objection API server.')
@click.option('--history-limit', '-H', required=False, type=click.IntRange(min=0), default=0, show_default=True,
help='Number of latest history entries to show at startup. Disabled by default.')
def explore(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File,
enable_api: bool) -> None:
enable_api: bool, history_limit: int) -> None:
"""
Deprecated: Use 'start' instead.
"""
Expand All @@ -228,7 +232,8 @@ def explore(plugin_folder: str, quiet: bool, startup_command: str, file_commands
startup_command=startup_command,
file_commands=file_commands,
startup_script=startup_script,
enable_api=enable_api)
enable_api=enable_api,
history_limit=history_limit)

@cli.command()
@click.option('--hook-debug', '-d', required=False, default=False, is_flag=True,
Expand Down
5 changes: 5 additions & 0 deletions objection/console/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@
}
},

'history': {
'meta': 'List and replay commands from the persistent command history',
'exec': None, # handled in the Repl class so it can access prompt history
},

'ls': {
'meta': 'List files in the current working directory',
'dynamic': filemanager.list_folders_in_current_fm_directory,
Expand Down
12 changes: 12 additions & 0 deletions objection/console/helpfiles/history.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Command: history

Usage: history

Lists commands from the persistent command history with a number in front of
each entry. Use !N or N to execute entry N. Startup history is disabled by
default; use --history-limit to show the latest entries when objection starts.
Use --history-limit 0 to disable the startup list explicitly.

Examples:
history
!69
147 changes: 144 additions & 3 deletions objection/console/repl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
import traceback

import click
Expand All @@ -13,13 +14,17 @@
from prompt_toolkit.styles import Style

from .commands import COMMANDS
from ..commands import command_history
from .completer import CommandCompleter
from ..__init__ import __version__
from ..state.app import app_state
from ..state.connection import state_connection
from ..utils.helpers import get_tokens


HISTORY_FILE = os.path.expanduser('~/.objection/objection_history')


class Repl(object):
"""
The exploration REPL for objection
Expand All @@ -30,6 +35,9 @@ def __init__(self) -> None:

self.completer = FuzzyCompleter(CommandCompleter())
self.commands_repository = COMMANDS
self.history = FileHistory(HISTORY_FILE)
self._history_timestamps = []
self._history_entries = self._load_history_entries()
self.session = self.get_prompt_session()

def get_prompt_session(self) -> PromptSession:
Expand All @@ -40,7 +48,7 @@ def get_prompt_session(self) -> PromptSession:
"""

return PromptSession(
history=FileHistory(os.path.expanduser('~/.objection/objection_history')),
history=self.history,
completer=self.completer,
style=self.get_prompt_style(),
auto_suggest=AutoSuggestFromHistory(),
Expand Down Expand Up @@ -111,8 +119,30 @@ def run_command(self, document: str) -> None:
if document.strip() == '':
return

document = document.strip()

# Both a bare number and the !N form refer to the full history list.
history_reference = self._resolve_history_reference(document)
if history_reference is not None:
history_number, historic_command = history_reference
if not historic_command:
return
click.secho('Running historic command {0}: {1}'.format(history_number, historic_command), dim=True)
self.run_command(historic_command)
return

# The top-level history command needs access to the prompt session's
# persistent history, so it is dispatched here rather than through the
# static command repository.
if document == 'history':
entries = self.get_history_entries()
timestamps = self._history_timestamps if len(self._history_timestamps) == len(entries) else []
command_history.numbered_history(entries, timestamps)
app_state.add_command_to_history(command=document)
return

# handle os commands
if document.strip().startswith('!'):
if document.startswith('!'):

# strip the leading !
os_cmd = document[1:]
Expand Down Expand Up @@ -170,6 +200,104 @@ def run_command(self, document: str) -> None:
exec_method(arguments)

app_state.add_command_to_history(command=document)
self._record_history_entry(document)

def _load_history_entries(self) -> list:
"""Load prompt history in chronological order."""

entries = []
timestamps = []
lines = []
timestamp = None

def add_entry() -> None:
if not lines:
return

command = ''.join(lines).strip()
if command:
entries.append(command)
timestamps.append(timestamp)

try:
with open(self.history.filename, 'r', encoding='utf-8', errors='replace') as history_file:
for line in history_file:
if line.startswith('# '):
add_entry()
lines.clear()
timestamp = line[2:].strip()
elif line.startswith('+'):
lines.append(line[1:])
else:
add_entry()
lines.clear()
timestamp = None

add_entry()
except OSError:
return []

self._history_timestamps = timestamps
return entries

def _append_runtime_history_entry(self, command: str) -> None:
"""Track a command executed outside prompt_toolkit's FileHistory."""

command = command.strip()
if not command:
return

if not self._history_entries or self._history_entries[-1] != command:
self._history_entries.append(command)
self._history_timestamps.append(None)

def _record_history_entry(self, command: str) -> None:
"""Track commands executed outside prompt_toolkit (for example startup commands)."""

# Commands entered at the prompt have already been stored by
# prompt_toolkit. Avoid adding those entries twice while still making
# programmatically executed startup commands available for replay.
self._append_runtime_history_entry(command)

def get_history_entries(self) -> list:
"""Return the persistent command history in chronological order."""

return list(self._history_entries)

def get_startup_history(self, limit: int = 3) -> list:
"""Return the latest history entries with their full-history numbers."""

if limit <= 0:
return []

entries = self.get_history_entries()
first_entry = max(0, len(entries) - limit)
return [(number, entries[number - 1])
for number in range(first_entry + 1, len(entries) + 1)]

def _resolve_history_reference(self, document: str):
"""Resolve !N against full history and N against startup favourites."""

full_history = document.startswith('!') and re.fullmatch(r'!\d+', document)
bare_number = re.fullmatch(r'\d+', document)
if not full_history and not bare_number:
return None

try:
number = int(document[1:] if full_history else document)
except ValueError:
return None

if number < 1:
click.secho('History entries start at 1.', fg='yellow')
return (number, '')

entries = self.get_history_entries()
if number <= len(entries):
return number, entries[number - 1]

click.secho('No history entry found for: {0}'.format(number), fg='yellow')
return (number, '')

def _find_command_exec_method(self, tokens: list) -> tuple:
"""
Expand Down Expand Up @@ -342,7 +470,7 @@ def handle_reconnect(document: str) -> bool:

return False

def run(self, quiet: bool) -> None:
def run(self, quiet: bool, history_limit: int = 0) -> None:
"""
Start the objection repl.
"""
Expand All @@ -360,6 +488,19 @@ def run(self, quiet: bool) -> None:

if not quiet:
click.secho(banner, bold=True)
startup_history = self.get_startup_history(history_limit)
if history_limit > 0:
click.secho('* Historic commands', fg='white', dim=True)
if startup_history:
timestamps = (self._history_timestamps
if len(self._history_timestamps) == len(self._history_entries) else [])
for number, command in startup_history:
timestamp = timestamps[number - 1] if number <= len(timestamps) else None
prefix = ('{0} '.format(command_history.format_history_timestamp(timestamp))
if timestamp else '')
click.secho('{0} {1}{2}'.format(number, prefix, command))
else:
click.secho('No historic commands found.', dim=True)
click.secho('[tab] for command suggestions', fg='white', dim=True)

# the main application loop is here, reading inputs provided by
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "objection"
version = "1.12.5"
version = "1.13.0"
description = "Instrumented Mobile Pentest Framework"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
Loading