dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev41__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +50 -0
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/target.py +1 -1
- dissect/target/tools/fs.py +25 -65
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +5 -1
- dissect/target/tools/shell.py +473 -347
- dissect/target/tools/utils.py +9 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/METADATA +9 -6
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/RECORD +18 -17
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/WHEEL +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev41.dist-info}/top_level.txt +0 -0
dissect/target/tools/shell.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import argparse
|
2
4
|
import cmd
|
3
5
|
import contextlib
|
4
|
-
import datetime
|
5
6
|
import fnmatch
|
6
7
|
import io
|
7
8
|
import itertools
|
@@ -13,27 +14,33 @@ import random
|
|
13
14
|
import re
|
14
15
|
import shlex
|
15
16
|
import shutil
|
16
|
-
import stat
|
17
17
|
import subprocess
|
18
18
|
import sys
|
19
19
|
from contextlib import contextmanager
|
20
|
-
from
|
20
|
+
from datetime import datetime, timedelta, timezone
|
21
|
+
from typing import Any, BinaryIO, Callable, Iterator, TextIO
|
21
22
|
|
22
23
|
from dissect.cstruct import hexdump
|
23
24
|
from flow.record import RecordOutput
|
24
25
|
|
25
26
|
from dissect.target.exceptions import (
|
26
|
-
FileNotFoundError,
|
27
27
|
PluginError,
|
28
28
|
RegistryError,
|
29
29
|
RegistryKeyNotFoundError,
|
30
30
|
RegistryValueNotFoundError,
|
31
31
|
TargetError,
|
32
32
|
)
|
33
|
-
from dissect.target.filesystem import FilesystemEntry
|
33
|
+
from dissect.target.filesystem import FilesystemEntry
|
34
34
|
from dissect.target.helpers import cyber, fsutil, regutil
|
35
|
-
from dissect.target.plugin import PluginFunction, arg
|
35
|
+
from dissect.target.plugin import PluginFunction, alias, arg, clone_alias
|
36
36
|
from dissect.target.target import Target
|
37
|
+
from dissect.target.tools.fsutils import (
|
38
|
+
fmt_ls_colors,
|
39
|
+
ls_scandir,
|
40
|
+
print_ls,
|
41
|
+
print_stat,
|
42
|
+
print_xattr,
|
43
|
+
)
|
37
44
|
from dissect.target.tools.info import print_target_info
|
38
45
|
from dissect.target.tools.utils import (
|
39
46
|
args_to_uri,
|
@@ -52,47 +59,23 @@ logging.raiseExceptions = False
|
|
52
59
|
try:
|
53
60
|
import readline
|
54
61
|
|
55
|
-
# remove
|
62
|
+
# remove `-`, `$` and `{` as an autocomplete delimeter on Linux
|
56
63
|
# https://stackoverflow.com/questions/27288340/python-cmd-on-linux-does-not-autocomplete-special-characters-or-symbols
|
57
|
-
readline.set_completer_delims(readline.get_completer_delims().replace("-", "").replace("$", ""))
|
64
|
+
readline.set_completer_delims(readline.get_completer_delims().replace("-", "").replace("$", "").replace("{", ""))
|
65
|
+
|
66
|
+
# Fix autocomplete on macOS
|
67
|
+
# https://stackoverflow.com/a/7116997
|
68
|
+
if "libedit" in readline.__doc__:
|
69
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
70
|
+
else:
|
71
|
+
readline.parse_and_bind("tab: complete")
|
58
72
|
except ImportError:
|
59
73
|
# Readline is not available on Windows
|
60
74
|
log.warning("Readline module is not available")
|
61
75
|
readline = None
|
62
76
|
|
63
|
-
# ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
|
64
|
-
STAT_TEMPLATE = """ File: {path} {symlink}
|
65
|
-
Size: {size} {filetype}
|
66
|
-
Inode: {inode} Links: {nlink}
|
67
|
-
Access: ({modeord}/{modestr}) Uid: ( {uid} ) Gid: ( {gid} )
|
68
|
-
Access: {atime}
|
69
|
-
Modify: {mtime}
|
70
|
-
Change: {ctime}"""
|
71
|
-
|
72
|
-
FALLBACK_LS_COLORS = "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32" # noqa: E501
|
73
|
-
|
74
|
-
|
75
|
-
def prepare_ls_colors() -> dict[str, str]:
|
76
|
-
"""Parse the LS_COLORS environment variable so we can use it later."""
|
77
|
-
d = {}
|
78
|
-
ls_colors = os.environ.get("LS_COLORS", FALLBACK_LS_COLORS)
|
79
|
-
for line in ls_colors.split(":"):
|
80
|
-
if not line:
|
81
|
-
continue
|
82
|
-
|
83
|
-
ft, _, value = line.partition("=")
|
84
|
-
if ft.startswith("*"):
|
85
|
-
ft = ft[1:]
|
86
|
-
|
87
|
-
d[ft] = f"\x1b[{value}m{{}}\x1b[0m"
|
88
77
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
LS_COLORS = prepare_ls_colors()
|
93
|
-
|
94
|
-
|
95
|
-
class TargetCmd(cmd.Cmd):
|
78
|
+
class ExtendedCmd(cmd.Cmd):
|
96
79
|
"""Subclassed cmd.Cmd to provide some additional features.
|
97
80
|
|
98
81
|
Add new simple commands by implementing:
|
@@ -112,49 +95,25 @@ class TargetCmd(cmd.Cmd):
|
|
112
95
|
|
113
96
|
CMD_PREFIX = "cmd_"
|
114
97
|
|
115
|
-
|
116
|
-
DEFAULT_HISTFILESIZE = 10_000
|
117
|
-
DEFAULT_HISTDIR = None
|
118
|
-
DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}"
|
119
|
-
|
120
|
-
def __init__(self, target: Target):
|
98
|
+
def __init__(self, cyber: bool = False):
|
121
99
|
cmd.Cmd.__init__(self)
|
122
|
-
self.target = target
|
123
100
|
self.debug = False
|
101
|
+
self.cyber = cyber
|
124
102
|
self.identchars += "."
|
125
103
|
|
126
|
-
self.
|
127
|
-
self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR)
|
128
|
-
|
129
|
-
if self.histdir:
|
130
|
-
self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT)
|
131
|
-
self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path(
|
132
|
-
self.histdirfmt.format(uid=os.getuid(), target=target.name)
|
133
|
-
)
|
134
|
-
else:
|
135
|
-
self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser()
|
136
|
-
|
137
|
-
def preloop(self) -> None:
|
138
|
-
if readline and self.histfile.exists():
|
139
|
-
try:
|
140
|
-
readline.read_history_file(self.histfile)
|
141
|
-
except Exception as e:
|
142
|
-
log.debug("Error reading history file: %s", e)
|
143
|
-
|
144
|
-
def postloop(self) -> None:
|
145
|
-
if readline:
|
146
|
-
readline.set_history_length(self.histfilesize)
|
147
|
-
try:
|
148
|
-
readline.write_history_file(self.histfile)
|
149
|
-
except Exception as e:
|
150
|
-
log.debug("Error writing history file: %s", e)
|
104
|
+
self.register_aliases()
|
151
105
|
|
152
106
|
def __getattr__(self, attr: str) -> Any:
|
153
107
|
if attr.startswith("help_"):
|
154
108
|
_, _, command = attr.partition("_")
|
109
|
+
|
110
|
+
def print_help(command: str, func: Callable) -> None:
|
111
|
+
parser = generate_argparse_for_bound_method(func, usage_tmpl=f"{command} {{usage}}")
|
112
|
+
parser.print_help()
|
113
|
+
|
155
114
|
try:
|
156
115
|
func = getattr(self, self.CMD_PREFIX + command)
|
157
|
-
return lambda:
|
116
|
+
return lambda: print_help(command, func)
|
158
117
|
except AttributeError:
|
159
118
|
pass
|
160
119
|
|
@@ -164,6 +123,16 @@ class TargetCmd(cmd.Cmd):
|
|
164
123
|
def check_compatible(target: Target) -> bool:
|
165
124
|
return True
|
166
125
|
|
126
|
+
def register_aliases(self) -> None:
|
127
|
+
for name in self.get_names():
|
128
|
+
if name.startswith(self.CMD_PREFIX):
|
129
|
+
func = getattr(self.__class__, name)
|
130
|
+
for alias_name in getattr(func, "__aliases__", []):
|
131
|
+
if not alias_name.startswith(self.CMD_PREFIX):
|
132
|
+
alias_name = self.CMD_PREFIX + alias_name
|
133
|
+
|
134
|
+
clone_alias(self.__class__, func, alias_name)
|
135
|
+
|
167
136
|
def get_names(self) -> list[str]:
|
168
137
|
names = cmd.Cmd.get_names(self)
|
169
138
|
|
@@ -175,44 +144,49 @@ class TargetCmd(cmd.Cmd):
|
|
175
144
|
|
176
145
|
return names
|
177
146
|
|
178
|
-
def
|
147
|
+
def _handle_command(self, line: str) -> bool | None:
|
148
|
+
"""Check whether custom handling of the cmd can be performed and if so, do it.
|
149
|
+
|
150
|
+
If a custom handling of the cmd was performed, return the result (a boolean indicating whether the shell should
|
151
|
+
exit). If not, return None. Can be overridden by subclasses to perform further / other 'custom command' checks.
|
152
|
+
"""
|
179
153
|
if line == "EOF":
|
180
154
|
return True
|
181
155
|
|
182
|
-
# Override default command execution to first attempt complex
|
183
|
-
# command execution, and then target plugin command execution
|
156
|
+
# Override default command execution to first attempt complex command execution
|
184
157
|
command, command_args_str, line = self.parseline(line)
|
185
158
|
|
186
|
-
|
159
|
+
if hasattr(self, self.CMD_PREFIX + command):
|
187
160
|
return self._exec_command(command, command_args_str)
|
188
|
-
except AttributeError:
|
189
|
-
pass
|
190
161
|
|
191
|
-
if
|
192
|
-
|
162
|
+
# Return None if no custom command was found to be run
|
163
|
+
return None
|
193
164
|
|
194
|
-
|
165
|
+
def default(self, line: str) -> bool:
|
166
|
+
if (should_exit := self._handle_command(line)) is not None:
|
167
|
+
return should_exit
|
168
|
+
|
169
|
+
# Fallback to default
|
170
|
+
cmd.Cmd.default(self, line)
|
171
|
+
return False
|
195
172
|
|
196
173
|
def emptyline(self) -> None:
|
197
174
|
"""This function forces Python's cmd.Cmd module to behave like a regular shell.
|
198
175
|
|
199
176
|
When entering an empty command, the cmd module will by default repeat the previous command.
|
200
177
|
By defining an empty ``emptyline`` function we make sure no command is executed instead.
|
201
|
-
|
178
|
+
Resources:
|
179
|
+
- https://stackoverflow.com/a/16479030
|
180
|
+
- https://github.com/python/cpython/blob/3.12/Lib/cmd.py#L10
|
202
181
|
"""
|
203
182
|
pass
|
204
183
|
|
205
|
-
def _exec(
|
206
|
-
self, func: Callable[[list[str], TextIO], bool], command_args_str: str, no_cyber: bool = False
|
207
|
-
) -> Optional[bool]:
|
184
|
+
def _exec(self, func: Callable[[list[str], TextIO], bool], command_args_str: str, no_cyber: bool = False) -> bool:
|
208
185
|
"""Command execution helper that chains initial command and piped subprocesses (if any) together."""
|
209
186
|
|
210
187
|
argparts = []
|
211
188
|
if command_args_str is not None:
|
212
|
-
|
213
|
-
lexer.wordchars += "$"
|
214
|
-
lexer.whitespace_split = True
|
215
|
-
argparts = list(lexer)
|
189
|
+
argparts = arg_str_to_arg_list(command_args_str)
|
216
190
|
|
217
191
|
if "|" in argparts:
|
218
192
|
pipeidx = argparts.index("|")
|
@@ -223,15 +197,16 @@ class TargetCmd(cmd.Cmd):
|
|
223
197
|
except OSError as e:
|
224
198
|
# in case of a failure in a subprocess
|
225
199
|
print(e)
|
200
|
+
return False
|
226
201
|
else:
|
227
202
|
ctx = contextlib.nullcontext()
|
228
|
-
if self.
|
203
|
+
if self.cyber and not no_cyber:
|
229
204
|
ctx = cyber.cyber(color=None, run_at_end=True)
|
230
205
|
|
231
206
|
with ctx:
|
232
207
|
return func(argparts, sys.stdout)
|
233
208
|
|
234
|
-
def _exec_command(self, command: str, command_args_str: str) ->
|
209
|
+
def _exec_command(self, command: str, command_args_str: str) -> bool:
|
235
210
|
"""Command execution helper for ``cmd_`` commands."""
|
236
211
|
cmdfunc = getattr(self, self.CMD_PREFIX + command)
|
237
212
|
argparser = generate_argparse_for_bound_method(cmdfunc, usage_tmpl=f"{command} {{usage}}")
|
@@ -240,21 +215,122 @@ class TargetCmd(cmd.Cmd):
|
|
240
215
|
try:
|
241
216
|
args = argparser.parse_args(argparts)
|
242
217
|
except SystemExit:
|
243
|
-
return
|
218
|
+
return False
|
244
219
|
return cmdfunc(args, stdout)
|
245
220
|
|
246
221
|
# These commands enter a subshell, which doesn't work well with cyber
|
247
222
|
no_cyber = cmdfunc.__func__ in (TargetCli.cmd_registry, TargetCli.cmd_enter)
|
248
223
|
return self._exec(_exec_, command_args_str, no_cyber)
|
249
224
|
|
250
|
-
def
|
225
|
+
def do_man(self, line: str) -> bool:
|
226
|
+
"""alias for help"""
|
227
|
+
return self.do_help(line)
|
228
|
+
|
229
|
+
def complete_man(self, *args) -> list[str]:
|
230
|
+
return cmd.Cmd.complete_help(self, *args)
|
231
|
+
|
232
|
+
def do_clear(self, line: str) -> bool:
|
233
|
+
"""clear the terminal screen"""
|
234
|
+
os.system("cls||clear")
|
235
|
+
return False
|
236
|
+
|
237
|
+
def do_cls(self, line: str) -> bool:
|
238
|
+
"""alias for clear"""
|
239
|
+
return self.do_clear(line)
|
240
|
+
|
241
|
+
def do_exit(self, line: str) -> bool:
|
242
|
+
"""exit shell"""
|
243
|
+
return True
|
244
|
+
|
245
|
+
def do_cyber(self, line: str) -> bool:
|
246
|
+
"""cyber"""
|
247
|
+
self.cyber = not self.cyber
|
248
|
+
word, color = {False: ("D I S E N", cyber.Color.RED), True: ("E N", cyber.Color.YELLOW)}[self.cyber]
|
249
|
+
with cyber.cyber(color=color):
|
250
|
+
print(f"C Y B E R - M O D E - {word} G A G E D")
|
251
|
+
return False
|
252
|
+
|
253
|
+
def do_debug(self, line: str) -> bool:
|
254
|
+
"""toggle debug mode"""
|
255
|
+
self.debug = not self.debug
|
256
|
+
if self.debug:
|
257
|
+
print("Debug mode on")
|
258
|
+
else:
|
259
|
+
print("Debug mode off")
|
260
|
+
return False
|
261
|
+
|
262
|
+
|
263
|
+
class TargetCmd(ExtendedCmd):
|
264
|
+
DEFAULT_HISTFILE = "~/.dissect_history"
|
265
|
+
DEFAULT_HISTFILESIZE = 10_000
|
266
|
+
DEFAULT_HISTDIR = None
|
267
|
+
DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}"
|
268
|
+
|
269
|
+
def __init__(self, target: Target):
|
270
|
+
self.target = target
|
271
|
+
|
272
|
+
# history file
|
273
|
+
self.histfilesize = getattr(target._config, "HISTFILESIZE", self.DEFAULT_HISTFILESIZE)
|
274
|
+
self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR)
|
275
|
+
|
276
|
+
if self.histdir:
|
277
|
+
self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT)
|
278
|
+
self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path(
|
279
|
+
self.histdirfmt.format(uid=os.getuid(), target=target.name)
|
280
|
+
)
|
281
|
+
else:
|
282
|
+
self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser()
|
283
|
+
|
284
|
+
# prompt format
|
285
|
+
if ps1 := getattr(target._config, "PS1", None):
|
286
|
+
if "{cwd}" in ps1 and "{base}" in ps1:
|
287
|
+
self.prompt_ps1 = ps1
|
288
|
+
|
289
|
+
elif getattr(target._config, "NO_COLOR", None) or os.getenv("NO_COLOR"):
|
290
|
+
self.prompt_ps1 = "{base}:{cwd}$ "
|
291
|
+
|
292
|
+
else:
|
293
|
+
self.prompt_ps1 = "\x1b[1;32m{base}\x1b[0m:\x1b[1;34m{cwd}\x1b[0m$ "
|
294
|
+
|
295
|
+
super().__init__(self.target.props.get("cyber"))
|
296
|
+
|
297
|
+
def preloop(self) -> None:
|
298
|
+
if readline and self.histfile.exists():
|
299
|
+
try:
|
300
|
+
readline.read_history_file(self.histfile)
|
301
|
+
except Exception as e:
|
302
|
+
log.debug("Error reading history file: %s", e)
|
303
|
+
|
304
|
+
def postloop(self) -> None:
|
305
|
+
if readline:
|
306
|
+
readline.set_history_length(self.histfilesize)
|
307
|
+
try:
|
308
|
+
readline.write_history_file(self.histfile)
|
309
|
+
except Exception as e:
|
310
|
+
log.debug("Error writing history file: %s", e)
|
311
|
+
|
312
|
+
def _handle_command(self, line: str) -> bool | None:
|
313
|
+
if (should_exit := super()._handle_command(line)) is not None:
|
314
|
+
return should_exit
|
315
|
+
|
316
|
+
# The parent class has already attempted complex command execution, we now attempt target plugin command
|
317
|
+
# execution
|
318
|
+
command, command_args_str, line = self.parseline(line)
|
319
|
+
|
320
|
+
if plugins := list(find_and_filter_plugins(self.target, command, [])):
|
321
|
+
return self._exec_target(plugins, command_args_str)
|
322
|
+
|
323
|
+
# We didn't execute a function on the target
|
324
|
+
return None
|
325
|
+
|
326
|
+
def _exec_target(self, funcs: list[PluginFunction], command_args_str: str) -> bool:
|
251
327
|
"""Command exection helper for target plugins."""
|
252
328
|
|
253
|
-
def _exec_(argparts: list[str], stdout: TextIO) ->
|
329
|
+
def _exec_(argparts: list[str], stdout: TextIO) -> None:
|
254
330
|
try:
|
255
331
|
output, value, _ = execute_function_on_target(self.target, func, argparts)
|
256
332
|
except SystemExit:
|
257
|
-
return
|
333
|
+
return
|
258
334
|
|
259
335
|
if output == "record":
|
260
336
|
# if the command results are piped to another process,
|
@@ -276,45 +352,21 @@ class TargetCmd(cmd.Cmd):
|
|
276
352
|
else:
|
277
353
|
print(value, file=stdout)
|
278
354
|
|
279
|
-
result = None
|
280
355
|
for func in funcs:
|
281
356
|
try:
|
282
|
-
|
357
|
+
self._exec(_exec_, command_args_str)
|
283
358
|
except PluginError as err:
|
284
359
|
if self.debug:
|
285
360
|
raise err
|
286
361
|
self.target.log.error(err)
|
287
362
|
|
288
|
-
|
363
|
+
# Keep the shell open
|
364
|
+
return False
|
289
365
|
|
290
|
-
def do_python(self, line: str) ->
|
366
|
+
def do_python(self, line: str) -> bool:
|
291
367
|
"""drop into a Python shell"""
|
292
368
|
python_shell([self.target])
|
293
|
-
|
294
|
-
def do_clear(self, line: str) -> Optional[bool]:
|
295
|
-
"""clear the terminal screen"""
|
296
|
-
os.system("cls||clear")
|
297
|
-
|
298
|
-
def do_cyber(self, line: str) -> Optional[bool]:
|
299
|
-
"""cyber"""
|
300
|
-
self.target.props["cyber"] = not bool(self.target.props.get("cyber"))
|
301
|
-
word, color = {False: ("D I S E N", cyber.Color.RED), True: ("E N", cyber.Color.YELLOW)}[
|
302
|
-
self.target.props["cyber"]
|
303
|
-
]
|
304
|
-
with cyber.cyber(color=color):
|
305
|
-
print(f"C Y B E R - M O D E - {word} G A G E D")
|
306
|
-
|
307
|
-
def do_exit(self, line: str) -> Optional[bool]:
|
308
|
-
"""exit shell"""
|
309
|
-
return True
|
310
|
-
|
311
|
-
def do_debug(self, line: str) -> Optional[bool]:
|
312
|
-
"""toggle debug mode"""
|
313
|
-
self.debug = not self.debug
|
314
|
-
if self.debug:
|
315
|
-
print("Debug mode on")
|
316
|
-
else:
|
317
|
-
print("Debug mode off")
|
369
|
+
return False
|
318
370
|
|
319
371
|
|
320
372
|
class TargetHubCli(cmd.Cmd):
|
@@ -337,24 +389,26 @@ class TargetHubCli(cmd.Cmd):
|
|
337
389
|
|
338
390
|
self._clicache = {}
|
339
391
|
|
340
|
-
def default(self, line: str) ->
|
392
|
+
def default(self, line: str) -> bool:
|
341
393
|
if line == "EOF":
|
342
394
|
return True
|
343
395
|
|
344
|
-
|
396
|
+
cmd.Cmd.default(self, line)
|
397
|
+
return False
|
345
398
|
|
346
399
|
def emptyline(self) -> None:
|
347
400
|
pass
|
348
401
|
|
349
|
-
def do_exit(self, line: str) ->
|
402
|
+
def do_exit(self, line: str) -> bool:
|
350
403
|
"""exit shell"""
|
351
404
|
return True
|
352
405
|
|
353
|
-
def do_list(self, line: str) ->
|
406
|
+
def do_list(self, line: str) -> bool:
|
354
407
|
"""list the loaded targets"""
|
355
408
|
print("\n".join([f"{i:2d}: {e}" for i, e in enumerate(self._names)]))
|
409
|
+
return False
|
356
410
|
|
357
|
-
def do_enter(self, line: str) ->
|
411
|
+
def do_enter(self, line: str) -> bool:
|
358
412
|
"""enter a target by number or name"""
|
359
413
|
|
360
414
|
if line.isdigit():
|
@@ -364,24 +418,25 @@ class TargetHubCli(cmd.Cmd):
|
|
364
418
|
idx = self._names_lower.index(line.lower())
|
365
419
|
except ValueError:
|
366
420
|
print("Unknown name")
|
367
|
-
return
|
421
|
+
return False
|
368
422
|
|
369
423
|
if idx >= len(self.targets):
|
370
424
|
print("Unknown target")
|
371
|
-
return
|
425
|
+
return False
|
372
426
|
|
373
427
|
try:
|
374
428
|
cli = self._clicache[idx]
|
375
429
|
except KeyError:
|
376
430
|
target = self.targets[idx]
|
377
431
|
if not self._targetcli.check_compatible(target):
|
378
|
-
return
|
432
|
+
return False
|
379
433
|
|
380
434
|
cli = self._targetcli(self.targets[idx])
|
381
435
|
self._clicache[idx] = cli
|
382
436
|
|
383
437
|
print(f"Entering {idx}: {self._names[idx]}")
|
384
438
|
run_cli(cli)
|
439
|
+
return False
|
385
440
|
|
386
441
|
def complete_enter(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
387
442
|
if not text:
|
@@ -395,32 +450,41 @@ class TargetHubCli(cmd.Cmd):
|
|
395
450
|
|
396
451
|
return compl
|
397
452
|
|
398
|
-
def do_python(self, line: str) ->
|
453
|
+
def do_python(self, line: str) -> bool:
|
399
454
|
"""drop into a Python shell"""
|
400
455
|
python_shell(self.targets)
|
456
|
+
return False
|
401
457
|
|
402
458
|
|
403
459
|
class TargetCli(TargetCmd):
|
404
460
|
"""CLI for interacting with a target and browsing the filesystem."""
|
405
461
|
|
406
462
|
def __init__(self, target: Target):
|
463
|
+
self.prompt_base = _target_name(target)
|
464
|
+
|
407
465
|
TargetCmd.__init__(self, target)
|
408
|
-
self.prompt_base = target.name
|
409
|
-
self._clicache = {}
|
410
466
|
|
467
|
+
self._clicache = {}
|
411
468
|
self.cwd = None
|
412
469
|
self.chdir("/")
|
413
470
|
|
414
471
|
@property
|
415
472
|
def prompt(self) -> str:
|
416
|
-
return
|
473
|
+
return self.prompt_ps1.format(base=self.prompt_base, cwd=self.cwd)
|
417
474
|
|
418
|
-
def completedefault(self, text: str, line: str, begidx: int, endidx: int):
|
419
|
-
path = line[:begidx].rsplit(" ")[-1]
|
475
|
+
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
476
|
+
path = self.resolve_path(line[:begidx].rsplit(" ")[-1])
|
420
477
|
textlower = text.lower()
|
421
478
|
|
422
|
-
|
423
|
-
|
479
|
+
suggestions = []
|
480
|
+
for fpath, fname in ls_scandir(path):
|
481
|
+
if not fname.lower().startswith(textlower):
|
482
|
+
continue
|
483
|
+
|
484
|
+
# Add a trailing slash to directories, to allow for easier traversal of the filesystem
|
485
|
+
suggestion = f"{fname}/" if fpath.is_dir() else fname
|
486
|
+
suggestions.append(suggestion)
|
487
|
+
return suggestions
|
424
488
|
|
425
489
|
def resolve_path(self, path: str) -> fsutil.TargetPath:
|
426
490
|
if not path:
|
@@ -448,7 +512,7 @@ class TargetCli(TargetCmd):
|
|
448
512
|
# component
|
449
513
|
print(err)
|
450
514
|
|
451
|
-
def check_file(self, path: str) ->
|
515
|
+
def check_file(self, path: str) -> fsutil.TargetPath | None:
|
452
516
|
path = self.resolve_path(path)
|
453
517
|
if not path.exists():
|
454
518
|
print(f"{path}: No such file")
|
@@ -464,7 +528,7 @@ class TargetCli(TargetCmd):
|
|
464
528
|
|
465
529
|
return path
|
466
530
|
|
467
|
-
def check_dir(self, path: str) ->
|
531
|
+
def check_dir(self, path: str) -> fsutil.TargetPath | None:
|
468
532
|
path = self.resolve_path(path)
|
469
533
|
if not path.exists():
|
470
534
|
print(f"{path}: No such directory")
|
@@ -480,70 +544,51 @@ class TargetCli(TargetCmd):
|
|
480
544
|
|
481
545
|
return path
|
482
546
|
|
547
|
+
def check_path(self, path: str) -> fsutil.TargetPath | None:
|
548
|
+
path = self.resolve_path(path)
|
549
|
+
if not path.exists():
|
550
|
+
print(f"{path}: No such file or directory")
|
551
|
+
return
|
552
|
+
|
553
|
+
return path
|
554
|
+
|
483
555
|
def chdir(self, path: str) -> None:
|
484
556
|
"""Change directory to the given path."""
|
485
557
|
if path := self.check_dir(path):
|
486
558
|
self.cwd = path
|
487
559
|
|
488
|
-
def
|
489
|
-
"""List a directory for the given path."""
|
490
|
-
path = self.resolve_path(path)
|
491
|
-
result = []
|
492
|
-
|
493
|
-
if path.exists() and path.is_dir():
|
494
|
-
for file_ in path.iterdir():
|
495
|
-
file_type = None
|
496
|
-
if color:
|
497
|
-
if file_.is_symlink():
|
498
|
-
file_type = "ln"
|
499
|
-
elif file_.is_dir():
|
500
|
-
file_type = "di"
|
501
|
-
elif file_.is_file():
|
502
|
-
file_type = "fi"
|
503
|
-
|
504
|
-
result.append((file_, fmt_ls_colors(file_type, file_.name) if color else file_.name))
|
505
|
-
|
506
|
-
# If we happen to scan an NTFS filesystem see if any of the
|
507
|
-
# entries has an alternative data stream and also list them.
|
508
|
-
entry = file_.get()
|
509
|
-
if isinstance(entry, LayerFilesystemEntry):
|
510
|
-
if entry.entries.fs.__type__ == "ntfs":
|
511
|
-
attrs = entry.lattr()
|
512
|
-
for data_stream in attrs.DATA:
|
513
|
-
if data_stream.name != "":
|
514
|
-
name = f"{file_.name}:{data_stream.name}"
|
515
|
-
result.append((file_, fmt_ls_colors(file_type, name) if color else name))
|
516
|
-
|
517
|
-
result.sort(key=lambda e: e[0].name)
|
518
|
-
|
519
|
-
return result
|
520
|
-
|
521
|
-
def do_cd(self, line: str) -> Optional[bool]:
|
560
|
+
def do_cd(self, line: str) -> bool:
|
522
561
|
"""change directory"""
|
523
562
|
self.chdir(line)
|
563
|
+
return False
|
524
564
|
|
525
|
-
def do_pwd(self, line: str) ->
|
565
|
+
def do_pwd(self, line: str) -> bool:
|
526
566
|
"""print current directory"""
|
527
567
|
print(self.cwd)
|
568
|
+
return False
|
528
569
|
|
529
|
-
def do_disks(self, line: str) ->
|
570
|
+
def do_disks(self, line: str) -> bool:
|
530
571
|
"""print target disks"""
|
531
572
|
for d in self.target.disks:
|
532
573
|
print(str(d))
|
574
|
+
return False
|
533
575
|
|
534
|
-
def do_volumes(self, line: str) ->
|
576
|
+
def do_volumes(self, line: str) -> bool:
|
535
577
|
"""print target volumes"""
|
536
578
|
for v in self.target.volumes:
|
537
579
|
print(str(v))
|
580
|
+
return False
|
538
581
|
|
539
|
-
def do_filesystems(self, line: str) ->
|
582
|
+
def do_filesystems(self, line: str) -> bool:
|
540
583
|
"""print target filesystems"""
|
541
584
|
for fs in self.target.filesystems:
|
542
585
|
print(str(fs))
|
586
|
+
return False
|
543
587
|
|
544
|
-
def do_info(self, line: str) ->
|
588
|
+
def do_info(self, line: str) -> bool:
|
545
589
|
"""print target information"""
|
546
|
-
|
590
|
+
print_target_info(self.target)
|
591
|
+
return False
|
547
592
|
|
548
593
|
@arg("path", nargs="?")
|
549
594
|
@arg("-l", action="store_true")
|
@@ -552,129 +597,137 @@ class TargetCli(TargetCmd):
|
|
552
597
|
@arg("-R", "--recursive", action="store_true", help="recursively list subdirectories encountered")
|
553
598
|
@arg("-c", action="store_true", dest="use_ctime", help="show time when file status was last changed")
|
554
599
|
@arg("-u", action="store_true", dest="use_atime", help="show time of last access")
|
555
|
-
|
600
|
+
@alias("l")
|
601
|
+
@alias("dir")
|
602
|
+
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
556
603
|
"""list directory contents"""
|
557
604
|
|
558
605
|
path = self.resolve_path(args.path)
|
559
606
|
|
560
607
|
if args.use_ctime and args.use_atime:
|
561
608
|
print("can't specify -c and -u at the same time")
|
562
|
-
return
|
609
|
+
return False
|
563
610
|
|
564
|
-
if not path or not
|
565
|
-
return
|
611
|
+
if not path or not self.check_dir(path):
|
612
|
+
return False
|
566
613
|
|
567
|
-
|
614
|
+
if path.is_file():
|
615
|
+
print(args.path) # mimic ls behaviour
|
616
|
+
return False
|
568
617
|
|
569
|
-
|
570
|
-
|
571
|
-
subdirs = []
|
618
|
+
print_ls(path, 0, stdout, args.l, args.human_readable, args.recursive, args.use_ctime, args.use_atime)
|
619
|
+
return False
|
572
620
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
if not args.l:
|
582
|
-
for target_path, name in contents:
|
583
|
-
print(name, file=stdout)
|
584
|
-
if target_path.is_dir():
|
585
|
-
subdirs.append(target_path)
|
586
|
-
else:
|
587
|
-
if len(contents) > 1:
|
588
|
-
print(f"total {len(contents)}", file=stdout)
|
589
|
-
for target_path, name in contents:
|
590
|
-
self.print_extensive_file_stat(args=args, stdout=stdout, target_path=target_path, name=name)
|
591
|
-
if target_path.is_dir():
|
592
|
-
subdirs.append(target_path)
|
593
|
-
|
594
|
-
if args.recursive and subdirs:
|
595
|
-
for subdir in subdirs:
|
596
|
-
self._print_ls(args, subdir, depth + 1, stdout)
|
597
|
-
|
598
|
-
def print_extensive_file_stat(
|
599
|
-
self, args: argparse.Namespace, stdout: TextIO, target_path: fsutil.TargetPath, name: str
|
600
|
-
) -> None:
|
601
|
-
"""Print the file status."""
|
602
|
-
try:
|
603
|
-
entry = target_path.get()
|
604
|
-
stat = entry.lstat()
|
605
|
-
symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
|
606
|
-
show_time = stat.st_mtime
|
607
|
-
if args.use_ctime:
|
608
|
-
show_time = stat.st_ctime
|
609
|
-
elif args.use_atime:
|
610
|
-
show_time = stat.st_atime
|
611
|
-
utc_time = datetime.datetime.utcfromtimestamp(show_time).isoformat()
|
612
|
-
|
613
|
-
print(
|
614
|
-
f"{stat_modestr(stat)} {stat.st_uid:4d} {stat.st_gid:4d} {stat.st_size:6d} {utc_time} {name}{symlink}",
|
615
|
-
file=stdout,
|
616
|
-
)
|
621
|
+
@arg("path", nargs="?")
|
622
|
+
def cmd_ll(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
623
|
+
"""alias for ls -la"""
|
624
|
+
args = extend_args(args, self.cmd_ls)
|
625
|
+
args.l = True # noqa: E741
|
626
|
+
args.a = True
|
627
|
+
return self.cmd_ls(args, stdout)
|
617
628
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
629
|
+
@arg("path", nargs="?")
|
630
|
+
def cmd_tree(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
631
|
+
"""alias for ls -R"""
|
632
|
+
args = extend_args(args, self.cmd_ls)
|
633
|
+
args.recursive = True
|
634
|
+
return self.cmd_ls(args, stdout)
|
623
635
|
|
624
636
|
@arg("path", nargs="?")
|
625
|
-
@arg("-name", default="*")
|
626
|
-
@arg("-iname")
|
627
|
-
|
637
|
+
@arg("-name", default="*", help="path to match with")
|
638
|
+
@arg("-iname", help="like -name, but the match is case insensitive")
|
639
|
+
@arg("-atime", type=int, help="file was last accessed n*24 hours ago")
|
640
|
+
@arg("-mtime", type=int, help="file was last modified n*24 hours ago")
|
641
|
+
@arg("-ctime", type=int, help="file (windows) or metadata (unix) was last changed n*24 hours ago")
|
642
|
+
@arg("-btime", type=int, help="file was born n*24 hours ago (ext4)")
|
643
|
+
def cmd_find(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
628
644
|
"""search for files in a directory hierarchy"""
|
629
645
|
path = self.resolve_path(args.path)
|
630
|
-
if not path:
|
631
|
-
return
|
646
|
+
if not path or not self.check_dir(path):
|
647
|
+
return False
|
648
|
+
|
649
|
+
matches = []
|
650
|
+
now = datetime.now(tz=timezone.utc)
|
651
|
+
do_time_compare = any([args.mtime, args.atime])
|
632
652
|
|
633
653
|
if args.iname:
|
634
654
|
pattern = re.compile(fnmatch.translate(args.iname), re.IGNORECASE)
|
635
655
|
for f in path.rglob("*"):
|
636
656
|
if pattern.match(f.name):
|
637
|
-
print(f, file=stdout)
|
638
|
-
|
639
|
-
elif args.name:
|
657
|
+
matches.append(f) if do_time_compare else print(f, file=stdout)
|
658
|
+
else:
|
640
659
|
for f in path.rglob(args.name):
|
660
|
+
matches.append(f) if do_time_compare else print(f, file=stdout)
|
661
|
+
|
662
|
+
def compare(now: datetime, then_ts: float, offset: int) -> bool:
|
663
|
+
then = datetime.fromtimestamp(then_ts, tz=timezone.utc)
|
664
|
+
return now - timedelta(hours=offset * 24) > then
|
665
|
+
|
666
|
+
if do_time_compare:
|
667
|
+
for f in matches:
|
668
|
+
s = f.lstat()
|
669
|
+
|
670
|
+
if args.mtime and compare(now, s.st_mtime, offset=args.mtime):
|
671
|
+
continue
|
672
|
+
|
673
|
+
if args.atime and compare(now, s.st_atime, offset=args.atime):
|
674
|
+
continue
|
675
|
+
|
676
|
+
if args.ctime and compare(now, s.st_ctime, offset=args.ctime):
|
677
|
+
continue
|
678
|
+
|
679
|
+
if args.btime and compare(now, s.st_birthtime, offset=args.btime):
|
680
|
+
continue
|
681
|
+
|
641
682
|
print(f, file=stdout)
|
642
683
|
|
684
|
+
return False
|
685
|
+
|
643
686
|
@arg("path")
|
644
687
|
@arg("-L", "--dereference", action="store_true")
|
645
|
-
def cmd_stat(self, args: argparse.Namespace, stdout: TextIO) ->
|
688
|
+
def cmd_stat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
646
689
|
"""display file status"""
|
647
690
|
path = self.resolve_path(args.path)
|
648
|
-
if not path:
|
649
|
-
return
|
691
|
+
if not path or not self.check_path(path):
|
692
|
+
return False
|
693
|
+
|
694
|
+
print_stat(path, stdout, args.dereference)
|
695
|
+
return False
|
696
|
+
|
697
|
+
@arg("path")
|
698
|
+
@arg("-d", "--dump", action="store_true")
|
699
|
+
@arg("-R", "--recursive", action="store_true")
|
700
|
+
@alias("getfattr")
|
701
|
+
def cmd_attr(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
702
|
+
"""display file attributes"""
|
703
|
+
path = self.resolve_path(args.path)
|
704
|
+
if not path or not self.check_path(path):
|
705
|
+
return False
|
706
|
+
|
707
|
+
try:
|
708
|
+
if attr := path.get().attr():
|
709
|
+
print_xattr(path.name, attr, stdout)
|
710
|
+
except Exception:
|
711
|
+
pass
|
712
|
+
|
713
|
+
if args.recursive:
|
714
|
+
for child in path.rglob("*"):
|
715
|
+
try:
|
716
|
+
if child_attr := child.get().attr():
|
717
|
+
print_xattr(child, child_attr, stdout)
|
718
|
+
print()
|
719
|
+
except Exception:
|
720
|
+
pass
|
650
721
|
|
651
|
-
|
652
|
-
|
653
|
-
s = path.stat() if args.dereference else path.lstat()
|
654
|
-
|
655
|
-
res = STAT_TEMPLATE.format(
|
656
|
-
path=path,
|
657
|
-
symlink=symlink,
|
658
|
-
size=s.st_size,
|
659
|
-
filetype="",
|
660
|
-
inode=s.st_ino,
|
661
|
-
nlink=s.st_nlink,
|
662
|
-
modeord=oct(stat.S_IMODE(s.st_mode)),
|
663
|
-
modestr=stat_modestr(s),
|
664
|
-
uid=s.st_uid,
|
665
|
-
gid=s.st_gid,
|
666
|
-
atime=datetime.datetime.utcfromtimestamp(s.st_atime).isoformat(),
|
667
|
-
mtime=datetime.datetime.utcfromtimestamp(s.st_mtime).isoformat(),
|
668
|
-
ctime=datetime.datetime.utcfromtimestamp(s.st_ctime).isoformat(),
|
669
|
-
)
|
670
|
-
print(res, file=stdout)
|
722
|
+
return False
|
671
723
|
|
672
724
|
@arg("path")
|
673
|
-
def cmd_file(self, args: argparse.Namespace, stdout: TextIO) ->
|
725
|
+
def cmd_file(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
674
726
|
"""determine file type"""
|
727
|
+
|
675
728
|
path = self.check_file(args.path)
|
676
729
|
if not path:
|
677
|
-
return
|
730
|
+
return False
|
678
731
|
|
679
732
|
fh = path.open()
|
680
733
|
|
@@ -686,11 +739,12 @@ class TargetCli(TargetCmd):
|
|
686
739
|
p.wait()
|
687
740
|
filetype = p.stdout.read().decode().split(":", 1)[1].strip()
|
688
741
|
print(f"{path}: {filetype}", file=stdout)
|
742
|
+
return False
|
689
743
|
|
690
744
|
@arg("path", nargs="+")
|
691
745
|
@arg("-o", "--out", default=".")
|
692
746
|
@arg("-v", "--verbose", action="store_true")
|
693
|
-
def cmd_save(self, args: argparse.Namespace, stdout: TextIO) ->
|
747
|
+
def cmd_save(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
694
748
|
"""save a common file or directory to the host filesystem"""
|
695
749
|
dst_path = pathlib.Path(args.out).resolve()
|
696
750
|
|
@@ -719,7 +773,7 @@ class TargetCli(TargetCmd):
|
|
719
773
|
return diverging_path
|
720
774
|
|
721
775
|
def save_path(
|
722
|
-
src_path: pathlib.Path, dst_path: pathlib.Path, create_dst_subdir:
|
776
|
+
src_path: pathlib.Path, dst_path: pathlib.Path, create_dst_subdir: pathlib.Path | None = None
|
723
777
|
) -> None:
|
724
778
|
"""Save a common file or directory in src_path to dst_path.
|
725
779
|
|
@@ -798,8 +852,8 @@ class TargetCli(TargetCmd):
|
|
798
852
|
try:
|
799
853
|
first_src_path = next(src_paths)
|
800
854
|
except StopIteration:
|
801
|
-
print(f"{path}:
|
802
|
-
return
|
855
|
+
print(f"{path}: No such file or directory")
|
856
|
+
return False
|
803
857
|
|
804
858
|
try:
|
805
859
|
second_src_path = next(src_paths)
|
@@ -820,10 +874,18 @@ class TargetCli(TargetCmd):
|
|
820
874
|
extra_dir = get_diverging_path(src_path, reference_path).parent
|
821
875
|
save_path(src_path, dst_path, create_dst_subdir=extra_dir)
|
822
876
|
|
877
|
+
return False
|
878
|
+
|
823
879
|
@arg("path")
|
824
|
-
|
880
|
+
@alias("type")
|
881
|
+
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
825
882
|
"""print file content"""
|
826
|
-
paths = self.resolve_glob_path(args.path)
|
883
|
+
paths = list(self.resolve_glob_path(args.path))
|
884
|
+
|
885
|
+
if not paths:
|
886
|
+
print(f"{args.path}: No such file or directory")
|
887
|
+
return False
|
888
|
+
|
827
889
|
stdout = stdout.buffer
|
828
890
|
for path in paths:
|
829
891
|
path = self.check_file(path)
|
@@ -834,11 +896,17 @@ class TargetCli(TargetCmd):
|
|
834
896
|
shutil.copyfileobj(fh, stdout)
|
835
897
|
stdout.flush()
|
836
898
|
print("")
|
899
|
+
return False
|
837
900
|
|
838
901
|
@arg("path")
|
839
|
-
def cmd_zcat(self, args: argparse.Namespace, stdout: TextIO) ->
|
902
|
+
def cmd_zcat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
840
903
|
"""print file content from compressed files"""
|
841
|
-
paths = self.resolve_glob_path(args.path)
|
904
|
+
paths = list(self.resolve_glob_path(args.path))
|
905
|
+
|
906
|
+
if not paths:
|
907
|
+
print(f"{args.path}: No such file or directory")
|
908
|
+
return False
|
909
|
+
|
842
910
|
stdout = stdout.buffer
|
843
911
|
for path in paths:
|
844
912
|
path = self.check_file(path)
|
@@ -849,45 +917,70 @@ class TargetCli(TargetCmd):
|
|
849
917
|
shutil.copyfileobj(fh, stdout)
|
850
918
|
stdout.flush()
|
851
919
|
|
920
|
+
return False
|
921
|
+
|
852
922
|
@arg("path")
|
853
|
-
|
854
|
-
|
923
|
+
@arg("-n", "--length", type=int, default=16 * 20, help="amount of bytes to read")
|
924
|
+
@arg("-s", "--skip", type=int, default=0, help="skip offset bytes from the beginning")
|
925
|
+
@arg("-p", "--hex", action="store_true", default=False, help="output in plain hexdump style")
|
926
|
+
@arg("-C", "--canonical", action="store_true")
|
927
|
+
@alias("xxd")
|
928
|
+
def cmd_hexdump(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
929
|
+
"""print a hexdump of a file"""
|
855
930
|
path = self.check_file(args.path)
|
856
931
|
if not path:
|
857
|
-
return
|
932
|
+
return False
|
933
|
+
|
934
|
+
fh = path.open("rb")
|
935
|
+
if args.skip > 0:
|
936
|
+
fh.seek(args.skip + 1)
|
937
|
+
|
938
|
+
if args.hex:
|
939
|
+
print(fh.read(args.length).hex(), file=stdout)
|
940
|
+
else:
|
941
|
+
print(hexdump(fh.read(args.length), output="string"), file=stdout)
|
858
942
|
|
859
|
-
|
943
|
+
return False
|
860
944
|
|
861
945
|
@arg("path")
|
862
|
-
|
946
|
+
@alias("digest")
|
947
|
+
@alias("shasum")
|
948
|
+
def cmd_hash(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
863
949
|
"""print the MD5, SHA1 and SHA256 hashes of a file"""
|
864
950
|
path = self.check_file(args.path)
|
865
951
|
if not path:
|
866
|
-
return
|
952
|
+
return False
|
867
953
|
|
868
954
|
md5, sha1, sha256 = path.get().hash()
|
869
955
|
print(f"MD5:\t{md5}\nSHA1:\t{sha1}\nSHA256:\t{sha256}", file=stdout)
|
956
|
+
return False
|
870
957
|
|
871
958
|
@arg("path")
|
872
|
-
|
959
|
+
@alias("head")
|
960
|
+
@alias("more")
|
961
|
+
def cmd_less(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
873
962
|
"""open the first 10 MB of a file with less"""
|
874
963
|
path = self.check_file(args.path)
|
875
964
|
if not path:
|
876
|
-
return
|
965
|
+
return False
|
877
966
|
|
878
967
|
pydoc.pager(path.open("rt", errors="ignore").read(10 * 1024 * 1024))
|
968
|
+
return False
|
879
969
|
|
880
970
|
@arg("path")
|
881
|
-
|
971
|
+
@alias("zhead")
|
972
|
+
@alias("zmore")
|
973
|
+
def cmd_zless(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
882
974
|
"""open the first 10 MB of a compressed file with zless"""
|
883
975
|
path = self.check_file(args.path)
|
884
976
|
if not path:
|
885
|
-
return
|
977
|
+
return False
|
886
978
|
|
887
979
|
pydoc.pager(fsutil.open_decompress(path, "rt").read(10 * 1024 * 1024))
|
980
|
+
return False
|
888
981
|
|
889
982
|
@arg("path", nargs="+")
|
890
|
-
def cmd_readlink(self, args: argparse.Namespace, stdout: TextIO) ->
|
983
|
+
def cmd_readlink(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
891
984
|
"""print resolved symbolic links or canonical file names"""
|
892
985
|
for path in args.path:
|
893
986
|
path = self.resolve_path(path)
|
@@ -896,12 +989,14 @@ class TargetCli(TargetCmd):
|
|
896
989
|
|
897
990
|
print(path.get().readlink(), file=stdout)
|
898
991
|
|
992
|
+
return False
|
993
|
+
|
899
994
|
@arg("path", nargs="?", help="load a hive from the given path")
|
900
|
-
def cmd_registry(self, args: argparse.Namespace, stdout: TextIO) ->
|
995
|
+
def cmd_registry(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
901
996
|
"""drop into a registry shell"""
|
902
997
|
if self.target.os == "linux":
|
903
998
|
run_cli(UnixConfigTreeCli(self.target))
|
904
|
-
return
|
999
|
+
return False
|
905
1000
|
|
906
1001
|
hive = None
|
907
1002
|
|
@@ -909,7 +1004,7 @@ class TargetCli(TargetCmd):
|
|
909
1004
|
if args.path:
|
910
1005
|
path = self.check_file(args.path)
|
911
1006
|
if not path:
|
912
|
-
return
|
1007
|
+
return False
|
913
1008
|
|
914
1009
|
hive = regutil.RegfHive(path)
|
915
1010
|
clikey = f"registry_{path}"
|
@@ -918,26 +1013,26 @@ class TargetCli(TargetCmd):
|
|
918
1013
|
cli = self._clicache[clikey]
|
919
1014
|
except KeyError:
|
920
1015
|
if not hive and not RegistryCli.check_compatible(self.target):
|
921
|
-
return
|
1016
|
+
return False
|
922
1017
|
|
923
1018
|
cli = RegistryCli(self.target, hive)
|
924
1019
|
self._clicache[clikey] = cli
|
925
1020
|
|
926
1021
|
run_cli(cli)
|
927
|
-
|
928
|
-
# Print an additional empty newline after exit
|
929
1022
|
print()
|
1023
|
+
return False
|
930
1024
|
|
931
1025
|
@arg("targets", metavar="TARGETS", nargs="*", help="targets to load")
|
932
1026
|
@arg("-p", "--python", action="store_true", help="(I)Python shell")
|
933
1027
|
@arg("-r", "--registry", action="store_true", help="registry shell")
|
934
|
-
def cmd_enter(self, args: argparse.Namespace, stdout: TextIO) ->
|
1028
|
+
def cmd_enter(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
935
1029
|
"""load one or more files as sub-targets and drop into a sub-shell"""
|
936
1030
|
paths = [self.resolve_path(path) for path in args.targets]
|
937
1031
|
|
938
1032
|
if args.python:
|
939
1033
|
# Quick path that doesn't require CLI caching
|
940
|
-
|
1034
|
+
open_shell(paths, args.python, args.registry)
|
1035
|
+
return False
|
941
1036
|
|
942
1037
|
clikey = tuple(str(path) for path in paths)
|
943
1038
|
try:
|
@@ -946,33 +1041,32 @@ class TargetCli(TargetCmd):
|
|
946
1041
|
targets = list(Target.open_all(paths))
|
947
1042
|
cli = create_cli(targets, RegistryCli if args.registry else TargetCli)
|
948
1043
|
if not cli:
|
949
|
-
return
|
1044
|
+
return False
|
950
1045
|
|
951
1046
|
self._clicache[clikey] = cli
|
952
1047
|
|
953
1048
|
run_cli(cli)
|
954
|
-
|
955
|
-
# Print an additional empty newline after exit
|
956
1049
|
print()
|
1050
|
+
return False
|
957
1051
|
|
958
1052
|
|
959
1053
|
class UnixConfigTreeCli(TargetCli):
|
960
1054
|
def __init__(self, target: Target):
|
961
1055
|
TargetCmd.__init__(self, target)
|
962
1056
|
self.config_tree = target.etc()
|
963
|
-
self.prompt_base = target
|
1057
|
+
self.prompt_base = _target_name(target)
|
964
1058
|
|
965
1059
|
self.cwd = None
|
966
1060
|
self.chdir("/")
|
967
1061
|
|
968
1062
|
@property
|
969
1063
|
def prompt(self) -> str:
|
970
|
-
return f"{self.prompt_base}
|
1064
|
+
return f"(config tree) {self.prompt_base}:{self.cwd}$ "
|
971
1065
|
|
972
1066
|
def check_compatible(target: Target) -> bool:
|
973
1067
|
return target.has_function("etc")
|
974
1068
|
|
975
|
-
def resolve_path(self, path:
|
1069
|
+
def resolve_path(self, path: str | fsutil.TargetPath | None) -> fsutil.TargetPath:
|
976
1070
|
if not path:
|
977
1071
|
return self.cwd
|
978
1072
|
|
@@ -1006,12 +1100,12 @@ class UnixConfigTreeCli(TargetCli):
|
|
1006
1100
|
class RegistryCli(TargetCmd):
|
1007
1101
|
"""CLI for browsing the registry."""
|
1008
1102
|
|
1009
|
-
def __init__(self, target: Target, registry:
|
1010
|
-
|
1011
|
-
self.registry = registry or target.registry
|
1103
|
+
def __init__(self, target: Target, registry: regutil.RegfHive | None = None):
|
1104
|
+
self.prompt_base = _target_name(target)
|
1012
1105
|
|
1013
|
-
self
|
1106
|
+
TargetCmd.__init__(self, target)
|
1014
1107
|
|
1108
|
+
self.registry = registry or target.registry
|
1015
1109
|
self.cwd = None
|
1016
1110
|
self.chdir("\\")
|
1017
1111
|
|
@@ -1024,8 +1118,7 @@ class RegistryCli(TargetCmd):
|
|
1024
1118
|
|
1025
1119
|
@property
|
1026
1120
|
def prompt(self) -> str:
|
1027
|
-
|
1028
|
-
return f"{self.prompt_base}/registry {prompt_end}> "
|
1121
|
+
return "(registry) " + self.prompt_ps1.format(base=self.prompt_base, cwd=self.cwd)
|
1029
1122
|
|
1030
1123
|
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
1031
1124
|
path = line[:begidx].rsplit(" ")[-1]
|
@@ -1065,9 +1158,7 @@ class RegistryCli(TargetCmd):
|
|
1065
1158
|
if self.check_key(path):
|
1066
1159
|
self.cwd = "\\" + path.strip("\\")
|
1067
1160
|
|
1068
|
-
def scandir(
|
1069
|
-
self, path: str, color: bool = False
|
1070
|
-
) -> list[tuple[Union[regutil.RegistryKey, regutil.RegistryValue], str]]:
|
1161
|
+
def scandir(self, path: str, color: bool = False) -> list[tuple[regutil.RegistryKey | regutil.RegistryValue, str]]:
|
1071
1162
|
try:
|
1072
1163
|
key = self.resolve_key(path)
|
1073
1164
|
except RegistryError:
|
@@ -1083,56 +1174,95 @@ class RegistryCli(TargetCmd):
|
|
1083
1174
|
r.sort(key=lambda e: e[0].name)
|
1084
1175
|
return r
|
1085
1176
|
|
1086
|
-
def do_cd(self, line: str) ->
|
1177
|
+
def do_cd(self, line: str) -> bool:
|
1087
1178
|
"""change subkey"""
|
1179
|
+
if line == "..":
|
1180
|
+
try:
|
1181
|
+
self.resolve_key(self.cwd + "\\..")
|
1182
|
+
except RegistryError:
|
1183
|
+
self.do_up(line)
|
1184
|
+
return False
|
1185
|
+
|
1088
1186
|
self.chdir(line)
|
1187
|
+
return False
|
1089
1188
|
|
1090
|
-
def do_up(self, line: str) ->
|
1189
|
+
def do_up(self, line: str) -> bool:
|
1091
1190
|
"""go up a subkey"""
|
1092
1191
|
parent = self.cwd.rpartition("\\")[0]
|
1093
1192
|
if not parent:
|
1094
1193
|
parent = "\\"
|
1095
1194
|
self.chdir(parent)
|
1195
|
+
return False
|
1096
1196
|
|
1097
|
-
def do_pwd(self, line: str) ->
|
1197
|
+
def do_pwd(self, line: str) -> bool:
|
1098
1198
|
"""print current path"""
|
1099
|
-
print(self.cwd)
|
1199
|
+
print(self.cwd.lstrip("\\"))
|
1200
|
+
return False
|
1100
1201
|
|
1101
|
-
def do_recommend(self, line: str) ->
|
1202
|
+
def do_recommend(self, line: str) -> bool:
|
1102
1203
|
"""recommend a key"""
|
1103
1204
|
print(random.choice([name for _, name in self.scandir(None)]))
|
1205
|
+
return False
|
1104
1206
|
|
1105
1207
|
@arg("path", nargs="?")
|
1106
|
-
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) ->
|
1208
|
+
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1107
1209
|
key = self.check_key(args.path)
|
1108
1210
|
if not key:
|
1109
|
-
return
|
1211
|
+
return False
|
1110
1212
|
|
1111
1213
|
r = self.scandir(key, color=True)
|
1112
1214
|
print("\n".join([name for _, name in r]), file=stdout)
|
1215
|
+
return False
|
1113
1216
|
|
1114
1217
|
@arg("value")
|
1115
|
-
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) ->
|
1218
|
+
def cmd_cat(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1116
1219
|
value = self.check_value(args.value)
|
1117
1220
|
if not value:
|
1118
|
-
return
|
1221
|
+
return False
|
1119
1222
|
|
1120
1223
|
print(repr(value.value), file=stdout)
|
1224
|
+
return False
|
1225
|
+
|
1226
|
+
@arg("value")
|
1227
|
+
@arg("-p", "--hex", action="store_true")
|
1228
|
+
@alias("xxd")
|
1229
|
+
def cmd_hexdump(self, args: argparse.Namespace, stdout: TextIO) -> bool:
|
1230
|
+
value = self.check_value(args.value)
|
1231
|
+
if not value:
|
1232
|
+
return False
|
1121
1233
|
|
1234
|
+
if args.hex:
|
1235
|
+
print(value.value.hex(), file=stdout)
|
1236
|
+
else:
|
1237
|
+
print(hexdump(value.value, output="string"), file=stdout)
|
1122
1238
|
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1239
|
+
return False
|
1240
|
+
|
1241
|
+
|
1242
|
+
def arg_str_to_arg_list(args: str) -> list[str]:
|
1243
|
+
"""Convert a commandline string to a list of command line arguments."""
|
1244
|
+
lexer = shlex.shlex(args, posix=True, punctuation_chars=True)
|
1245
|
+
lexer.wordchars += "$"
|
1246
|
+
lexer.whitespace_split = True
|
1247
|
+
return list(lexer)
|
1129
1248
|
|
1130
|
-
try:
|
1131
|
-
return LS_COLORS[fsutil.splitext(name)[1]].format(name)
|
1132
|
-
except KeyError:
|
1133
|
-
pass
|
1134
1249
|
|
1135
|
-
|
1250
|
+
def extend_args(args: argparse.Namespace, func: Callable) -> argparse.Namespace:
|
1251
|
+
"""Extend the arguments of the given ``func`` with the provided ``argparse.Namespace``."""
|
1252
|
+
for short, kwargs in func.__args__:
|
1253
|
+
name = kwargs.get("dest", short[-1]).lstrip("-").replace("-", "_")
|
1254
|
+
if not hasattr(args, name):
|
1255
|
+
setattr(args, name, None)
|
1256
|
+
return args
|
1257
|
+
|
1258
|
+
|
1259
|
+
def _target_name(target: Target) -> str:
|
1260
|
+
"""Return a target name for cmd.Cmd base prompts."""
|
1261
|
+
|
1262
|
+
if target.has_function("domain") and target.domain:
|
1263
|
+
return f"{target.name}.{target.domain}"
|
1264
|
+
|
1265
|
+
return target.name
|
1136
1266
|
|
1137
1267
|
|
1138
1268
|
@contextmanager
|
@@ -1200,15 +1330,7 @@ def build_pipe_stdout(pipe_parts: list[str]) -> Iterator[TextIO]:
|
|
1200
1330
|
yield pipe_stdin
|
1201
1331
|
|
1202
1332
|
|
1203
|
-
def
|
1204
|
-
"""Helper method for generating a mode string from a numerical mode value."""
|
1205
|
-
is_dir = "d" if stat.S_ISDIR(st.st_mode) else "-"
|
1206
|
-
dic = {"7": "rwx", "6": "rw-", "5": "r-x", "4": "r--", "0": "---"}
|
1207
|
-
perm = str(oct(st.st_mode)[-3:])
|
1208
|
-
return is_dir + "".join(dic.get(x, x) for x in perm)
|
1209
|
-
|
1210
|
-
|
1211
|
-
def open_shell(targets: list[Union[str, pathlib.Path]], python: bool, registry: bool) -> None:
|
1333
|
+
def open_shell(targets: list[str | pathlib.Path], python: bool, registry: bool) -> None:
|
1212
1334
|
"""Helper method for starting a regular, Python or registry shell for one or multiple targets."""
|
1213
1335
|
targets = list(Target.open_all(targets))
|
1214
1336
|
|
@@ -1246,7 +1368,7 @@ def python_shell(targets: list[Target]) -> None:
|
|
1246
1368
|
print()
|
1247
1369
|
|
1248
1370
|
|
1249
|
-
def create_cli(targets: list[Target], cli_cls: type[TargetCmd]) ->
|
1371
|
+
def create_cli(targets: list[Target], cli_cls: type[TargetCmd]) -> cmd.Cmd | None:
|
1250
1372
|
"""Helper method for instatiating the appropriate CLI."""
|
1251
1373
|
if len(targets) == 1:
|
1252
1374
|
target = targets[0]
|
@@ -1275,6 +1397,9 @@ def run_cli(cli: cmd.Cmd) -> None:
|
|
1275
1397
|
return
|
1276
1398
|
|
1277
1399
|
except KeyboardInterrupt:
|
1400
|
+
# Run postloop so the interrupted command is added to the history file
|
1401
|
+
cli.postloop()
|
1402
|
+
|
1278
1403
|
# Add a line when pressing ctrl+c, so the next one starts at a new line
|
1279
1404
|
print()
|
1280
1405
|
|
@@ -1286,6 +1411,8 @@ def run_cli(cli: cmd.Cmd) -> None:
|
|
1286
1411
|
print(f"*** Unhandled error: {e}")
|
1287
1412
|
print("If you wish to see the full debug trace, enable debug mode.")
|
1288
1413
|
|
1414
|
+
cli.postloop()
|
1415
|
+
|
1289
1416
|
|
1290
1417
|
@catch_sigpipe
|
1291
1418
|
def main() -> None:
|
@@ -1311,8 +1438,7 @@ def main() -> None:
|
|
1311
1438
|
args.targets = args_to_uri(args.targets, args.loader, rest) if args.loader else args.targets
|
1312
1439
|
process_generic_arguments(args)
|
1313
1440
|
|
1314
|
-
# For the shell tool we want -q to log slightly more then just CRITICAL
|
1315
|
-
# messages.
|
1441
|
+
# For the shell tool we want -q to log slightly more then just CRITICAL messages.
|
1316
1442
|
if args.quiet:
|
1317
1443
|
logging.getLogger("dissect").setLevel(level=logging.ERROR)
|
1318
1444
|
|