dissect.target 3.14.dev28__py3-none-any.whl → 3.15__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. dissect/target/containers/ewf.py +1 -1
  2. dissect/target/containers/vhd.py +5 -2
  3. dissect/target/filesystem.py +36 -18
  4. dissect/target/filesystems/dir.py +10 -4
  5. dissect/target/filesystems/jffs.py +122 -0
  6. dissect/target/helpers/compat/path_310.py +506 -0
  7. dissect/target/helpers/compat/path_311.py +539 -0
  8. dissect/target/helpers/compat/path_312.py +443 -0
  9. dissect/target/helpers/compat/path_39.py +545 -0
  10. dissect/target/helpers/compat/path_common.py +223 -0
  11. dissect/target/helpers/cyber.py +512 -0
  12. dissect/target/helpers/fsutil.py +128 -666
  13. dissect/target/helpers/hashutil.py +17 -57
  14. dissect/target/helpers/keychain.py +9 -3
  15. dissect/target/helpers/loaderutil.py +1 -1
  16. dissect/target/helpers/mount.py +47 -4
  17. dissect/target/helpers/polypath.py +73 -0
  18. dissect/target/helpers/record_modifier.py +100 -0
  19. dissect/target/loader.py +2 -1
  20. dissect/target/loaders/asdf.py +2 -0
  21. dissect/target/loaders/cyber.py +37 -0
  22. dissect/target/loaders/log.py +14 -3
  23. dissect/target/loaders/raw.py +2 -0
  24. dissect/target/loaders/remote.py +12 -0
  25. dissect/target/loaders/tar.py +13 -0
  26. dissect/target/loaders/targetd.py +2 -0
  27. dissect/target/loaders/velociraptor.py +12 -3
  28. dissect/target/loaders/vmwarevm.py +2 -0
  29. dissect/target/plugin.py +272 -143
  30. dissect/target/plugins/apps/ssh/openssh.py +11 -54
  31. dissect/target/plugins/apps/ssh/opensshd.py +4 -3
  32. dissect/target/plugins/apps/ssh/putty.py +236 -0
  33. dissect/target/plugins/apps/ssh/ssh.py +58 -0
  34. dissect/target/plugins/apps/vpn/openvpn.py +6 -0
  35. dissect/target/plugins/apps/webserver/apache.py +309 -95
  36. dissect/target/plugins/apps/webserver/caddy.py +5 -2
  37. dissect/target/plugins/apps/webserver/citrix.py +82 -0
  38. dissect/target/plugins/apps/webserver/iis.py +9 -12
  39. dissect/target/plugins/apps/webserver/nginx.py +5 -2
  40. dissect/target/plugins/apps/webserver/webserver.py +25 -41
  41. dissect/target/plugins/child/wsl.py +1 -1
  42. dissect/target/plugins/filesystem/ntfs/mft.py +10 -0
  43. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +10 -0
  44. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +10 -0
  45. dissect/target/plugins/filesystem/ntfs/utils.py +28 -5
  46. dissect/target/plugins/filesystem/resolver.py +6 -4
  47. dissect/target/plugins/general/default.py +0 -2
  48. dissect/target/plugins/general/example.py +0 -1
  49. dissect/target/plugins/general/loaders.py +3 -5
  50. dissect/target/plugins/os/unix/_os.py +3 -3
  51. dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
  52. dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
  53. dissect/target/plugins/os/unix/generic.py +17 -10
  54. dissect/target/plugins/os/unix/linux/fortios/__init__.py +0 -0
  55. dissect/target/plugins/os/unix/linux/fortios/_os.py +534 -0
  56. dissect/target/plugins/os/unix/linux/fortios/generic.py +30 -0
  57. dissect/target/plugins/os/unix/linux/fortios/locale.py +109 -0
  58. dissect/target/plugins/os/windows/log/evt.py +1 -1
  59. dissect/target/plugins/os/windows/log/schedlgu.py +155 -0
  60. dissect/target/plugins/os/windows/regf/firewall.py +1 -1
  61. dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
  62. dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
  63. dissect/target/plugins/os/windows/registry.py +1 -1
  64. dissect/target/plugins/os/windows/sam.py +3 -0
  65. dissect/target/plugins/os/windows/sru.py +41 -28
  66. dissect/target/plugins/os/windows/tasks.py +5 -2
  67. dissect/target/target.py +7 -3
  68. dissect/target/tools/dd.py +7 -1
  69. dissect/target/tools/fs.py +8 -1
  70. dissect/target/tools/info.py +22 -15
  71. dissect/target/tools/mount.py +28 -3
  72. dissect/target/tools/query.py +146 -117
  73. dissect/target/tools/reg.py +21 -16
  74. dissect/target/tools/shell.py +30 -6
  75. dissect/target/tools/utils.py +28 -0
  76. dissect/target/volumes/bde.py +14 -10
  77. dissect/target/volumes/luks.py +18 -10
  78. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
  79. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/RECORD +85 -67
  80. dissect/target/plugins/os/unix/linux/fortigate/_os.py +0 -175
  81. /dissect/target/{plugins/os/unix/linux/fortigate → helpers/compat}/__init__.py +0 -0
  82. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
  83. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
  84. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
  85. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
  86. {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/top_level.txt +0 -0
@@ -14,13 +14,15 @@ from dissect.target import Target
14
14
  from dissect.target.exceptions import (
15
15
  FatalError,
16
16
  PluginNotFoundError,
17
+ TargetError,
17
18
  UnsupportedPluginError,
18
19
  )
19
- from dissect.target.helpers import cache, hashutil
20
+ from dissect.target.helpers import cache, record_modifier
20
21
  from dissect.target.loaders.targetd import ProxyLoader
21
22
  from dissect.target.plugin import PLUGINS, OSPlugin, Plugin, find_plugin_functions
22
23
  from dissect.target.report import ExecutionReport
23
24
  from dissect.target.tools.utils import (
25
+ args_to_uri,
24
26
  catch_sigpipe,
25
27
  configure_generic_arguments,
26
28
  execute_function_on_target,
@@ -80,6 +82,15 @@ def main():
80
82
  default=None,
81
83
  help="list (matching) available plugins and loaders",
82
84
  )
85
+
86
+ parser.add_argument(
87
+ "-L",
88
+ "--loader",
89
+ action="store",
90
+ default=None,
91
+ help="select a specific loader (i.e. vmx, raw)",
92
+ )
93
+
83
94
  parser.add_argument("-s", "--strings", action="store_true", help="print output as string")
84
95
  parser.add_argument("-d", "--delimiter", default=" ", action="store", metavar="','")
85
96
  parser.add_argument("-j", "--json", action="store_true", help="output records as json")
@@ -100,7 +111,8 @@ def main():
100
111
  ),
101
112
  )
102
113
  parser.add_argument("--cmdb", action="store_true")
103
- parser.add_argument("--hash", action="store_true", help="hash all uri paths in records")
114
+ parser.add_argument("--hash", action="store_true", help="hash all paths in records")
115
+ parser.add_argument("--resolve", action="store_true", help="resolve all paths in records")
104
116
  parser.add_argument(
105
117
  "--report-dir",
106
118
  type=pathlib.Path,
@@ -110,6 +122,9 @@ def main():
110
122
 
111
123
  args, rest = parser.parse_known_args()
112
124
 
125
+ # If loader is specified then map to uri
126
+ targets = args_to_uri(args.targets, args.loader, rest) if args.loader else args.targets
127
+
113
128
  # Show help for target-query
114
129
  if not args.function and ("-h" in rest or "--help" in rest):
115
130
  parser.print_help()
@@ -134,7 +149,7 @@ def main():
134
149
 
135
150
  # Show help for a function or in general
136
151
  if "-h" in rest or "--help" in rest:
137
- found_functions, _ = find_plugin_functions(Target(), args.function, False)
152
+ found_functions, _ = find_plugin_functions(None, args.function, compatibility=False)
138
153
  if not len(found_functions):
139
154
  parser.error("function(s) not found, see -l for available plugins")
140
155
  func = found_functions[0]
@@ -156,16 +171,16 @@ def main():
156
171
  if args.list:
157
172
  collected_plugins = {}
158
173
 
159
- if args.targets:
160
- for target in args.targets:
174
+ if targets:
175
+ for target in targets:
161
176
  plugin_target = Target.open(target)
162
177
  if isinstance(plugin_target._loader, ProxyLoader):
163
178
  parser.error("can't list compatible plugins for remote targets.")
164
- funcs, _ = find_plugin_functions(plugin_target, args.list, True, show_hidden=True)
179
+ funcs, _ = find_plugin_functions(plugin_target, args.list, compatibility=True, show_hidden=True)
165
180
  for func in funcs:
166
181
  collected_plugins[func.path] = func.plugin_desc
167
182
  else:
168
- funcs, _ = find_plugin_functions(Target(), args.list, False, show_hidden=True)
183
+ funcs, _ = find_plugin_functions(Target(), args.list, compatibility=False, show_hidden=True)
169
184
  for func in funcs:
170
185
  collected_plugins[func.path] = func.plugin_desc
171
186
 
@@ -178,13 +193,13 @@ def main():
178
193
  target.plugins(list(collected_plugins.values()))
179
194
 
180
195
  # No real targets specified, show the available loaders
181
- if not args.targets:
196
+ if not targets:
182
197
  fparser = generate_argparse_for_bound_method(target.loaders, usage_tmpl=USAGE_FORMAT_TMPL)
183
198
  fargs, rest = fparser.parse_known_args(rest)
184
199
  target.loaders(**vars(fargs))
185
200
  parser.exit()
186
201
 
187
- if not args.targets:
202
+ if not targets:
188
203
  parser.error("too few arguments")
189
204
 
190
205
  if not args.function:
@@ -203,13 +218,13 @@ def main():
203
218
  # The only scenario that might cause this is with
204
219
  # custom plugins with idiosyncratic output across OS-versions/branches.
205
220
  output_types = set()
206
- funcs, invalid_funcs = find_plugin_functions(Target(), args.function, compatibility=False)
221
+ funcs, invalid_funcs = find_plugin_functions(None, args.function, compatibility=False)
207
222
 
208
223
  if any(invalid_funcs):
209
224
  parser.error(f"argument -f/--function contains invalid plugin(s): {', '.join(invalid_funcs)}")
210
225
 
211
226
  excluded_funcs, invalid_excluded_funcs = find_plugin_functions(
212
- Target(),
227
+ None,
213
228
  args.excluded_functions,
214
229
  compatibility=False,
215
230
  )
@@ -219,10 +234,10 @@ def main():
219
234
  f"argument -xf/--excluded-functions contains invalid plugin(s): {', '.join(invalid_excluded_funcs)}",
220
235
  )
221
236
 
222
- excluded_func_names = {excluded_func.name for excluded_func in excluded_funcs}
237
+ excluded_func_paths = {excluded_func.path for excluded_func in excluded_funcs}
223
238
 
224
239
  for func in funcs:
225
- if func.name in excluded_func_names:
240
+ if func.path in excluded_func_paths:
226
241
  continue
227
242
  output_types.add(func.output_type)
228
243
 
@@ -240,124 +255,135 @@ def main():
240
255
  execution_report.set_cli_args(args)
241
256
  execution_report.set_event_callbacks(Target)
242
257
 
243
- for target in Target.open_all(args.targets, args.children):
244
- if args.child:
245
- try:
246
- target = target.open_child(args.child)
247
- except Exception:
248
- target.log.exception("Exception while opening child '%s'", args.child)
258
+ try:
259
+ for target in Target.open_all(targets, args.children):
260
+ if args.child:
261
+ try:
262
+ target = target.open_child(args.child)
263
+ except Exception:
264
+ target.log.exception("Exception while opening child '%s'", args.child)
249
265
 
250
- if args.dry_run:
251
- print(f"Dry run on: {target}")
266
+ if args.dry_run:
267
+ print(f"Dry run on: {target}")
252
268
 
253
- record_entries = []
254
- basic_entries = []
255
- yield_entries = []
269
+ record_entries = []
270
+ basic_entries = []
271
+ yield_entries = []
256
272
 
257
- # Keep a set of plugins that were already executed on the target.
258
- executed_plugins = set()
273
+ # Keep a set of plugins that were already executed on the target.
274
+ executed_plugins = set()
259
275
 
260
- first_seen_output_type = default_output_type
261
- cli_params_unparsed = rest
276
+ first_seen_output_type = default_output_type
277
+ cli_params_unparsed = rest
262
278
 
263
- func_defs, _ = find_plugin_functions(target, args.function, compatibility=False)
264
- excluded_funcs, _ = find_plugin_functions(target, args.excluded_functions, compatibility=False)
265
- excluded_func_names = {excluded_func.name for excluded_func in excluded_funcs}
279
+ func_defs, _ = find_plugin_functions(target, args.function, compatibility=False)
280
+ excluded_funcs, _ = find_plugin_functions(target, args.excluded_functions, compatibility=False)
281
+ excluded_func_paths = {excluded_func.path for excluded_func in excluded_funcs}
266
282
 
267
- for func_def in func_defs:
268
- if func_def.name in excluded_func_names:
269
- continue
283
+ for func_def in func_defs:
284
+ if func_def.path in excluded_func_paths:
285
+ continue
270
286
 
271
- # Avoid executing same plugin for multiple OSes (like hostname)
272
- if func_def.name in executed_plugins:
273
- continue
274
- executed_plugins.add(func_def.name)
275
-
276
- # If the default type is record (meaning we skip everything else)
277
- # and actual output type is not record, continue.
278
- # We perform this check here because plugins that require output files/dirs
279
- # will exit if we attempt to exec them without (because they are implied by the wildcard).
280
- # Also this saves cycles of course.
281
- if default_output_type == "record" and func_def.output_type != "record":
282
- continue
287
+ # Avoid executing same plugin for multiple OSes (like hostname)
288
+ if func_def.name in executed_plugins:
289
+ continue
290
+ executed_plugins.add(func_def.name)
283
291
 
284
- if args.dry_run:
285
- print(f" execute: {func_def.name} ({func_def.path})")
286
- continue
292
+ # If the default type is record (meaning we skip everything else)
293
+ # and actual output type is not record, continue.
294
+ # We perform this check here because plugins that require output files/dirs
295
+ # will exit if we attempt to exec them without (because they are implied by the wildcard).
296
+ # Also this saves cycles of course.
297
+ if default_output_type == "record" and func_def.output_type != "record":
298
+ continue
287
299
 
288
- try:
289
- output_type, result, cli_params_unparsed = execute_function_on_target(
290
- target, func_def, cli_params_unparsed
291
- )
292
- except UnsupportedPluginError as e:
293
- target.log.error(
294
- "Unsupported plugin for %s: %s",
295
- func_def.name,
296
- e.root_cause_str(),
297
- )
298
-
299
- target.log.debug("%s", func_def, exc_info=e)
300
- continue
301
- except PluginNotFoundError:
302
- target.log.error("Cannot find plugin `%s`", func_def)
303
- continue
304
- except FatalError as fatal:
305
- fatal.emit_last_message(target.log.error)
306
- parser.exit(1)
307
- except Exception:
308
- target.log.error("Exception while executing function `%s`", func_def, exc_info=True)
309
- continue
300
+ if args.dry_run:
301
+ print(f" execute: {func_def.name} ({func_def.path})")
302
+ continue
310
303
 
311
- if first_seen_output_type and output_type != first_seen_output_type:
312
- target.log.error(
313
- (
314
- "Can't mix functions that generate different outputs: output type `%s` from `%s` "
315
- "does not match first seen output `%s`."
316
- ),
317
- output_type,
318
- func_def,
319
- first_seen_output_type,
320
- )
321
- parser.exit()
322
-
323
- if not first_seen_output_type:
324
- first_seen_output_type = output_type
325
-
326
- if output_type == "record":
327
- record_entries.append(result)
328
- elif output_type == "yield":
329
- yield_entries.append(result)
330
- elif output_type == "none":
331
- target.log.info("No result for function `%s` (output type is set to 'none')", func_def)
304
+ try:
305
+ output_type, result, cli_params_unparsed = execute_function_on_target(
306
+ target, func_def, cli_params_unparsed
307
+ )
308
+ except UnsupportedPluginError as e:
309
+ target.log.error(
310
+ "Unsupported plugin for %s: %s",
311
+ func_def.name,
312
+ e.root_cause_str(),
313
+ )
314
+
315
+ target.log.debug("%s", func_def, exc_info=e)
316
+ continue
317
+ except PluginNotFoundError:
318
+ target.log.error("Cannot find plugin `%s`", func_def)
319
+ continue
320
+ except FatalError as fatal:
321
+ fatal.emit_last_message(target.log.error)
322
+ parser.exit(1)
323
+ except Exception:
324
+ target.log.error("Exception while executing function `%s`", func_def, exc_info=True)
325
+ continue
326
+
327
+ if first_seen_output_type and output_type != first_seen_output_type:
328
+ target.log.error(
329
+ (
330
+ "Can't mix functions that generate different outputs: output type `%s` from `%s` "
331
+ "does not match first seen output `%s`."
332
+ ),
333
+ output_type,
334
+ func_def,
335
+ first_seen_output_type,
336
+ )
337
+ parser.exit()
338
+
339
+ if not first_seen_output_type:
340
+ first_seen_output_type = output_type
341
+
342
+ if output_type == "record":
343
+ record_entries.append(result)
344
+ elif output_type == "yield":
345
+ yield_entries.append(result)
346
+ elif output_type == "none":
347
+ target.log.info("No result for function `%s` (output type is set to 'none')", func_def)
348
+ continue
349
+ else:
350
+ basic_entries.append(result)
351
+
352
+ # Write basic functions
353
+ if len(basic_entries) > 0:
354
+ basic_entries_delim = args.delimiter.join(map(str, basic_entries))
355
+ if not args.cmdb:
356
+ print(f"{target} {basic_entries_delim}")
357
+ else:
358
+ print(f"{target.path}{args.delimiter}{basic_entries_delim}")
359
+
360
+ # Write yield functions
361
+ for entry in yield_entries:
362
+ for e in entry:
363
+ print(e)
364
+
365
+ # Write records
366
+ count = 0
367
+ break_out = False
368
+
369
+ modifier_type = None
370
+
371
+ if args.resolve:
372
+ modifier_type = record_modifier.Modifier.RESOLVE
373
+
374
+ if args.hash:
375
+ modifier_type = record_modifier.Modifier.HASH
376
+
377
+ modifier_func = record_modifier.get_modifier_function(modifier_type)
378
+
379
+ if not record_entries:
332
380
  continue
333
- else:
334
- basic_entries.append(result)
335
-
336
- # Write basic functions
337
- if len(basic_entries) > 0:
338
- basic_entries_delim = args.delimiter.join(map(str, basic_entries))
339
- if not args.cmdb:
340
- print(f"{target} {basic_entries_delim}")
341
- else:
342
- print(f"{target.path}{args.delimiter}{basic_entries_delim}")
343
-
344
- # Write yield functions
345
- for entry in yield_entries:
346
- for e in entry:
347
- print(e)
348
-
349
- # Write records
350
- count = 0
351
- break_out = False
352
- if len(record_entries):
381
+
353
382
  rs = record_output(args.strings, args.json)
354
383
  for entry in record_entries:
355
384
  try:
356
385
  for record_entries in entry:
357
- if args.hash:
358
- rs.write(hashutil.hash_path_records(target, record_entries))
359
- else:
360
- rs.write(record_entries)
386
+ rs.write(modifier_func(target, record_entries))
361
387
  count += 1
362
388
  if args.limit is not None and count >= args.limit:
363
389
  break_out = True
@@ -366,12 +392,15 @@ def main():
366
392
  # Ignore errors if multiple functions
367
393
  if len(funcs) > 1:
368
394
  target.log.error(f"Exception occurred while processing output of {func}", exc_info=e)
369
- pass
370
395
  else:
371
396
  raise e
372
397
 
373
398
  if break_out:
374
399
  break
400
+ except TargetError as e:
401
+ log.error(e)
402
+ log.debug("", exc_info=e)
403
+ parser.exit(1)
375
404
 
376
405
  timestamp = datetime.utcnow()
377
406
 
@@ -6,7 +6,7 @@ import argparse
6
6
  import logging
7
7
 
8
8
  from dissect.target import Target
9
- from dissect.target.exceptions import RegistryError
9
+ from dissect.target.exceptions import RegistryError, TargetError
10
10
  from dissect.target.tools.utils import (
11
11
  catch_sigpipe,
12
12
  configure_generic_arguments,
@@ -36,23 +36,28 @@ def main():
36
36
 
37
37
  process_generic_arguments(args)
38
38
 
39
- for target in Target.open_all(args.targets):
40
- try:
41
- if args.value:
42
- for key in target.registry.keys(args.key):
39
+ try:
40
+ for target in Target.open_all(args.targets):
41
+ try:
42
+ if args.value:
43
+ for key in target.registry.keys(args.key):
44
+ try:
45
+ print(key.value(args.value))
46
+ except RegistryError:
47
+ continue
48
+ else:
43
49
  try:
44
- print(key.value(args.value))
50
+ print(target)
51
+ for key in target.registry.keys(args.key):
52
+ recursor(key, args.depth, 0)
45
53
  except RegistryError:
46
- continue
47
- else:
48
- try:
49
- print(target)
50
- for key in target.registry.keys(args.key):
51
- recursor(key, args.depth, 0)
52
- except RegistryError:
53
- log.exception("Failed to find registry value")
54
- except Exception:
55
- log.exception("Failed to iterate key")
54
+ log.exception("Failed to find registry value")
55
+ except Exception:
56
+ log.exception("Failed to iterate key")
57
+ except TargetError as e:
58
+ log.error(e)
59
+ log.debug("", exc_info=e)
60
+ parser.exit(1)
56
61
 
57
62
 
58
63
  def recursor(key, depth, indent):
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import cmd
3
+ import contextlib
3
4
  import datetime
4
5
  import fnmatch
5
6
  import io
@@ -28,9 +29,10 @@ from dissect.target.exceptions import (
28
29
  RegistryError,
29
30
  RegistryKeyNotFoundError,
30
31
  RegistryValueNotFoundError,
32
+ TargetError,
31
33
  )
32
34
  from dissect.target.filesystem import FilesystemEntry, RootFilesystemEntry
33
- from dissect.target.helpers import fsutil, regutil
35
+ from dissect.target.helpers import cyber, fsutil, regutil
34
36
  from dissect.target.plugin import arg
35
37
  from dissect.target.target import Target
36
38
  from dissect.target.tools.info import print_target_info
@@ -164,7 +166,9 @@ class TargetCmd(cmd.Cmd):
164
166
  """
165
167
  pass
166
168
 
167
- def _exec(self, func: Callable[[list[str], TextIO], bool], command_args_str: str) -> Optional[bool]:
169
+ def _exec(
170
+ self, func: Callable[[list[str], TextIO], bool], command_args_str: str, no_cyber: bool = False
171
+ ) -> Optional[bool]:
168
172
  """Command execution helper that chains initial command and piped subprocesses (if any) together."""
169
173
 
170
174
  argparts = []
@@ -185,7 +189,12 @@ class TargetCmd(cmd.Cmd):
185
189
  # in case of a failure in a subprocess
186
190
  print(e)
187
191
  else:
188
- return func(argparts, sys.stdout)
192
+ ctx = contextlib.nullcontext()
193
+ if self.target.props.get("cyber") and not no_cyber:
194
+ ctx = cyber.cyber(color=None, run_at_end=True)
195
+
196
+ with ctx:
197
+ return func(argparts, sys.stdout)
189
198
  except IOError:
190
199
  pass
191
200
 
@@ -201,7 +210,9 @@ class TargetCmd(cmd.Cmd):
201
210
  return
202
211
  return cmdfunc(args, stdout)
203
212
 
204
- return self._exec(_exec_, command_args_str)
213
+ # These commands enter a subshell, which doesn't work well with cyber
214
+ no_cyber = cmdfunc.__func__ in (TargetCli.cmd_registry, TargetCli.cmd_enter)
215
+ return self._exec(_exec_, command_args_str, no_cyber)
205
216
 
206
217
  def _exec_target(self, func: str, command_args_str: str) -> Optional[bool]:
207
218
  """Command exection helper for target plugins."""
@@ -254,6 +265,15 @@ class TargetCmd(cmd.Cmd):
254
265
  """clear the terminal screen"""
255
266
  os.system("cls||clear")
256
267
 
268
+ def do_cyber(self, line: str) -> Optional[bool]:
269
+ """cyber"""
270
+ self.target.props["cyber"] = not bool(self.target.props.get("cyber"))
271
+ word, color = {False: ("D I S E N", cyber.Color.RED), True: ("E N", cyber.Color.YELLOW)}[
272
+ self.target.props["cyber"]
273
+ ]
274
+ with cyber.cyber(color=color):
275
+ print(f"C Y B E R - M O D E - {word} G A G E D")
276
+
257
277
  def do_exit(self, line: str) -> Optional[bool]:
258
278
  """exit shell"""
259
279
  return True
@@ -491,7 +511,7 @@ class TargetCli(TargetCmd):
491
511
  @arg("-l", action="store_true")
492
512
  @arg("-a", "--all", action="store_true") # ignored but included for proper argument parsing
493
513
  @arg("-h", "--human-readable", action="store_true")
494
- def cmd_ls(self, args: argparse.Namespace, stdout) -> Optional[bool]:
514
+ def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> Optional[bool]:
495
515
  """list directory contents"""
496
516
 
497
517
  path = self.resolve_path(args.path)
@@ -1214,7 +1234,11 @@ def main() -> None:
1214
1234
  if args.quiet:
1215
1235
  logging.getLogger("dissect").setLevel(level=logging.ERROR)
1216
1236
 
1217
- open_shell(args.targets, args.python, args.registry)
1237
+ try:
1238
+ open_shell(args.targets, args.python, args.registry)
1239
+ except TargetError as e:
1240
+ log.error(e)
1241
+ log.debug("", exc_info=e)
1218
1242
 
1219
1243
 
1220
1244
  if __name__ == "__main__":
@@ -4,6 +4,8 @@ import inspect
4
4
  import json
5
5
  import os
6
6
  import sys
7
+ import textwrap
8
+ import urllib
7
9
  from datetime import datetime
8
10
  from functools import wraps
9
11
  from pathlib import Path
@@ -12,7 +14,9 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, U
12
14
  from dissect.target import Target
13
15
  from dissect.target.exceptions import UnsupportedPluginError
14
16
  from dissect.target.helpers import docs, keychain
17
+ from dissect.target.helpers.docs import get_docstring
15
18
  from dissect.target.helpers.targetd import CommandProxy
19
+ from dissect.target.loader import LOADERS_BY_SCHEME
16
20
  from dissect.target.plugin import (
17
21
  OSPlugin,
18
22
  Plugin,
@@ -261,3 +265,27 @@ def catch_sigpipe(func: Callable) -> Callable:
261
265
  raise
262
266
 
263
267
  return wrapper
268
+
269
+
270
+ def args_to_uri(targets: list[str], loader_name: str, rest: list[str]) -> list[str]:
271
+ """Converts argument-style ``-L`` to URI-style.
272
+
273
+ Turns:
274
+ ``target-query /evtxs/* -L log --log-hint="evtx" -f evtx``
275
+ into:
276
+ ``target-query "log:///evtxs/*?hint=evtx" -f evtx``
277
+
278
+ For loaders providing ``@arg()`` arguments.
279
+ """
280
+ loader = LOADERS_BY_SCHEME.get(loader_name, None)
281
+
282
+ parser = argparse.ArgumentParser(
283
+ argument_default=argparse.SUPPRESS, description="\n".join(textwrap.wrap(get_docstring(loader)))
284
+ )
285
+ for load_arg in getattr(loader, "__args__", []):
286
+ parser.add_argument(*load_arg[0], **load_arg[1])
287
+ args = vars(parser.parse_known_args(rest)[0])
288
+ uris = []
289
+ for target in targets:
290
+ uris.append(f"{loader_name}://{target}" + (("?" + urllib.parse.urlencode(args)) if args else ""))
291
+ return uris
@@ -52,23 +52,26 @@ class BitlockerVolumeSystem(EncryptedVolumeSystem):
52
52
  **volume_details,
53
53
  )
54
54
 
55
- def unlock_with_passphrase(self, passphrase: str) -> None:
55
+ def unlock_with_passphrase(self, passphrase: str, is_wildcard: bool = False) -> None:
56
56
  try:
57
57
  self.bde.unlock_with_passphrase(passphrase)
58
58
  log.debug("Unlocked BDE volume with provided passphrase")
59
59
  except ValueError:
60
- log.exception("Failed to unlock BDE volume with provided passphrase")
60
+ if not is_wildcard:
61
+ log.exception("Failed to unlock BDE volume with provided passphrase")
61
62
 
62
- def unlock_with_recovery_key(self, recovery_key: str) -> None:
63
+ def unlock_with_recovery_key(self, recovery_key: str, is_wildcard: bool = False) -> None:
63
64
  try:
64
65
  self.bde.unlock_with_recovery_password(recovery_key)
65
66
  log.debug("Unlocked BDE volume with recovery key")
66
67
  except ValueError:
67
- log.exception("Failed to unlock BDE volume with recovery password")
68
+ if not is_wildcard:
69
+ log.exception("Failed to unlock BDE volume with recovery password")
68
70
 
69
- def unlock_with_bek_file(self, bek_file: pathlib.Path) -> None:
71
+ def unlock_with_bek_file(self, bek_file: pathlib.Path, is_wildcard: bool = False) -> None:
70
72
  if not bek_file.exists():
71
- log.error("Provided BEK file does not exist: %s", bek_file)
73
+ if not is_wildcard:
74
+ log.error("Provided BEK file does not exist: %s", bek_file)
72
75
  return
73
76
 
74
77
  with bek_file.open(mode="rb") as fh:
@@ -76,7 +79,8 @@ class BitlockerVolumeSystem(EncryptedVolumeSystem):
76
79
  self.bde.unlock_with_bek(fh)
77
80
  log.debug("Unlocked BDE volume with BEK file %s", bek_file)
78
81
  except ValueError:
79
- log.exception("Failed to unlock BDE volume with BEK file %s", bek_file)
82
+ if not is_wildcard:
83
+ log.exception("Failed to unlock BDE volume with BEK file %s", bek_file)
80
84
 
81
85
  def unlock_volume(self) -> AlignedStream:
82
86
  if self.bde.has_clear_key():
@@ -87,12 +91,12 @@ class BitlockerVolumeSystem(EncryptedVolumeSystem):
87
91
 
88
92
  for key in keys:
89
93
  if key.key_type == KeyType.PASSPHRASE and self.bde.has_passphrase():
90
- self.unlock_with_passphrase(key.value)
94
+ self.unlock_with_passphrase(key.value, key.is_wildcard)
91
95
  elif key.key_type == KeyType.RECOVERY_KEY and self.bde.has_recovery_password():
92
- self.unlock_with_recovery_key(key.value)
96
+ self.unlock_with_recovery_key(key.value, key.is_wildcard)
93
97
  elif key.key_type == KeyType.FILE:
94
98
  bek_file = pathlib.Path(key.value)
95
- self.unlock_with_bek_file(bek_file)
99
+ self.unlock_with_bek_file(bek_file, key.is_wildcard)
96
100
 
97
101
  if self.bde.unlocked:
98
102
  log.info("Volume %s with identifiers %s unlocked with %s", self.fh, identifiers, key)