Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add target-shell history #786

Merged
merged 6 commits into from
Aug 7, 2024
Merged
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
29 changes: 22 additions & 7 deletions dissect/target/helpers/config.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
from __future__ import annotations

import ast
import importlib.machinery
import importlib.util
import logging
from pathlib import Path
from types import ModuleType
from typing import Optional, Union

log = logging.getLogger(__name__)

CONFIG_NAME = ".targetcfg.py"


def load(path: Optional[Union[Path, str]]) -> ModuleType:
def load(paths: list[Path | str] | Path | str | None) -> ModuleType:
"""Attempt to load one configuration from the provided path(s)."""

if isinstance(paths, Path) or isinstance(paths, str):
paths = [paths]

config_spec = importlib.machinery.ModuleSpec("config", None)
config = importlib.util.module_from_spec(config_spec)
config_file = _find_config_file(path)
config_file = _find_config_file(paths)

if config_file:
config_values = _parse_ast(config_file.read_bytes())
config.__dict__.update(config_values)

return config


def _parse_ast(code: str) -> dict[str, Union[str, int]]:
def _parse_ast(code: str) -> dict[str, str | int]:
# Only allow basic value assignments for backwards compatibility
obj = {}

Expand Down Expand Up @@ -49,15 +57,19 @@ def _parse_ast(code: str) -> dict[str, Union[str, int]]:
return obj


def _find_config_file(path: Optional[Union[Path, str]]) -> Optional[Path]:
"""Find a config file anywhere in the given path and return it.
def _find_config_file(paths: list[Path | str] | None) -> Path | None:
"""Find a config file anywhere in the given path(s) and return it.

This algorithm allows parts of the path to not exist or the last part to be a filename.
It also does not look in the root directory ('/') for config files.
"""

if not paths:
return

config_file = None
if path:

for path in paths:
path = Path(path)
cur_path = path.absolute()

Expand All @@ -69,4 +81,7 @@ def _find_config_file(path: Optional[Union[Path, str]]) -> Optional[Path]:
config_file = cur_config
cur_path = cur_path.parent

if config_file:
break

return config_file
5 changes: 3 additions & 2 deletions dissect/target/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ def __init__(self, path: Union[str, Path] = None):
self._applied = False

try:
self._config = config.load(self.path)
self._config = config.load([self.path, os.getcwd()])
except Exception as e:
self.log.debug("Error loading config file", exc_info=e)
self.log.warning("Error loading config file: %s", self.path)
self.log.debug("", exc_info=e)
self._config = config.load(None) # This loads an empty config.

# Fill the disks and/or volumes and/or filesystems and apply() will
Expand Down
36 changes: 34 additions & 2 deletions dissect/target/tools/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
except ImportError:
# Readline is not available on Windows
log.warning("Readline module is not available")
readline = None

# ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
STAT_TEMPLATE = """ File: {path} {symlink}
Expand Down Expand Up @@ -111,12 +112,43 @@

CMD_PREFIX = "cmd_"

DEFAULT_HISTFILE = "~/.dissect_history"
DEFAULT_HISTFILESIZE = 10_000
DEFAULT_HISTDIR = None
DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}"

def __init__(self, target: Target):
cmd.Cmd.__init__(self)
self.target = target
self.debug = False
self.identchars += "."

self.histfilesize = getattr(target._config, "HISTFILESIZE", self.DEFAULT_HISTFILESIZE)
self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR)

if self.histdir:
self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT)
self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path(

Check warning on line 131 in dissect/target/tools/shell.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/tools/shell.py#L130-L131

Added lines #L130 - L131 were not covered by tests
self.histdirfmt.format(uid=os.getuid(), target=target.name)
)
else:
self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser()

def preloop(self) -> None:
if readline and self.histfile.exists():
try:
readline.read_history_file(self.histfile)
except Exception as e:
log.debug("Error reading history file: %s", e)

Check warning on line 142 in dissect/target/tools/shell.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/tools/shell.py#L141-L142

Added lines #L141 - L142 were not covered by tests

def postloop(self) -> None:
if readline:
readline.set_history_length(self.histfilesize)
try:
readline.write_history_file(self.histfile)
except Exception as e:
log.debug("Error writing history file: %s", e)

def __getattr__(self, attr: str) -> Any:
if attr.startswith("help_"):
_, _, command = attr.partition("_")
Expand Down Expand Up @@ -1241,18 +1273,18 @@
# Print an empty newline on exit
print()
return

except KeyboardInterrupt:
# Add a line when pressing ctrl+c, so the next one starts at a new line
print()
pass

except Exception as e:
if cli.debug:
log.exception(e)
else:
log.info(e)
print(f"*** Unhandled error: {e}")
print("If you wish to see the full debug trace, enable debug mode.")
pass


@catch_sigpipe
Expand Down