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.
- dissect/target/containers/ewf.py +1 -1
- dissect/target/containers/vhd.py +5 -2
- dissect/target/filesystem.py +36 -18
- dissect/target/filesystems/dir.py +10 -4
- dissect/target/filesystems/jffs.py +122 -0
- dissect/target/helpers/compat/path_310.py +506 -0
- dissect/target/helpers/compat/path_311.py +539 -0
- dissect/target/helpers/compat/path_312.py +443 -0
- dissect/target/helpers/compat/path_39.py +545 -0
- dissect/target/helpers/compat/path_common.py +223 -0
- dissect/target/helpers/cyber.py +512 -0
- dissect/target/helpers/fsutil.py +128 -666
- dissect/target/helpers/hashutil.py +17 -57
- dissect/target/helpers/keychain.py +9 -3
- dissect/target/helpers/loaderutil.py +1 -1
- dissect/target/helpers/mount.py +47 -4
- dissect/target/helpers/polypath.py +73 -0
- dissect/target/helpers/record_modifier.py +100 -0
- dissect/target/loader.py +2 -1
- dissect/target/loaders/asdf.py +2 -0
- dissect/target/loaders/cyber.py +37 -0
- dissect/target/loaders/log.py +14 -3
- dissect/target/loaders/raw.py +2 -0
- dissect/target/loaders/remote.py +12 -0
- dissect/target/loaders/tar.py +13 -0
- dissect/target/loaders/targetd.py +2 -0
- dissect/target/loaders/velociraptor.py +12 -3
- dissect/target/loaders/vmwarevm.py +2 -0
- dissect/target/plugin.py +272 -143
- dissect/target/plugins/apps/ssh/openssh.py +11 -54
- dissect/target/plugins/apps/ssh/opensshd.py +4 -3
- dissect/target/plugins/apps/ssh/putty.py +236 -0
- dissect/target/plugins/apps/ssh/ssh.py +58 -0
- dissect/target/plugins/apps/vpn/openvpn.py +6 -0
- dissect/target/plugins/apps/webserver/apache.py +309 -95
- dissect/target/plugins/apps/webserver/caddy.py +5 -2
- dissect/target/plugins/apps/webserver/citrix.py +82 -0
- dissect/target/plugins/apps/webserver/iis.py +9 -12
- dissect/target/plugins/apps/webserver/nginx.py +5 -2
- dissect/target/plugins/apps/webserver/webserver.py +25 -41
- dissect/target/plugins/child/wsl.py +1 -1
- dissect/target/plugins/filesystem/ntfs/mft.py +10 -0
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +10 -0
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +10 -0
- dissect/target/plugins/filesystem/ntfs/utils.py +28 -5
- dissect/target/plugins/filesystem/resolver.py +6 -4
- dissect/target/plugins/general/default.py +0 -2
- dissect/target/plugins/general/example.py +0 -1
- dissect/target/plugins/general/loaders.py +3 -5
- dissect/target/plugins/os/unix/_os.py +3 -3
- dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
- dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
- dissect/target/plugins/os/unix/generic.py +17 -10
- dissect/target/plugins/os/unix/linux/fortios/__init__.py +0 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +534 -0
- dissect/target/plugins/os/unix/linux/fortios/generic.py +30 -0
- dissect/target/plugins/os/unix/linux/fortios/locale.py +109 -0
- dissect/target/plugins/os/windows/log/evt.py +1 -1
- dissect/target/plugins/os/windows/log/schedlgu.py +155 -0
- dissect/target/plugins/os/windows/regf/firewall.py +1 -1
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
- dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
- dissect/target/plugins/os/windows/registry.py +1 -1
- dissect/target/plugins/os/windows/sam.py +3 -0
- dissect/target/plugins/os/windows/sru.py +41 -28
- dissect/target/plugins/os/windows/tasks.py +5 -2
- dissect/target/target.py +7 -3
- dissect/target/tools/dd.py +7 -1
- dissect/target/tools/fs.py +8 -1
- dissect/target/tools/info.py +22 -15
- dissect/target/tools/mount.py +28 -3
- dissect/target/tools/query.py +146 -117
- dissect/target/tools/reg.py +21 -16
- dissect/target/tools/shell.py +30 -6
- dissect/target/tools/utils.py +28 -0
- dissect/target/volumes/bde.py +14 -10
- dissect/target/volumes/luks.py +18 -10
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/RECORD +85 -67
- dissect/target/plugins/os/unix/linux/fortigate/_os.py +0 -175
- /dissect/target/{plugins/os/unix/linux/fortigate → helpers/compat}/__init__.py +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.14.dev28.dist-info → dissect.target-3.15.dist-info}/top_level.txt +0 -0
dissect/target/plugin.py
CHANGED
@@ -64,7 +64,7 @@ class OperatingSystem(StrEnum):
|
|
64
64
|
ANDROID = "android"
|
65
65
|
VYOS = "vyos"
|
66
66
|
IOS = "ios"
|
67
|
-
|
67
|
+
FORTIOS = "fortios"
|
68
68
|
CITRIX = "citrix-netscaler"
|
69
69
|
|
70
70
|
|
@@ -215,8 +215,6 @@ class Plugin:
|
|
215
215
|
produce redundant results when used with a wild card
|
216
216
|
(browser.* -> browser.history + browser.*.history).
|
217
217
|
"""
|
218
|
-
__skip__: bool = False
|
219
|
-
"""Prevents plugin functions from indexing this plugin at all."""
|
220
218
|
|
221
219
|
def __init_subclass__(cls, **kwargs):
|
222
220
|
super().__init_subclass__(**kwargs)
|
@@ -294,18 +292,43 @@ class OSPlugin(Plugin):
|
|
294
292
|
This provides a base class for certain common functions of OS's, which each OS plugin has to implement separately.
|
295
293
|
|
296
294
|
For example, it provides an interface for retrieving the hostname and users of a target.
|
295
|
+
|
296
|
+
All derived classes MUST implement ALL the classmethods and exported
|
297
|
+
methods with the same ``@classmethod`` or ``@export(...)`` annotation.
|
297
298
|
"""
|
298
299
|
|
300
|
+
def __init_subclass__(cls, **kwargs):
|
301
|
+
# Note that cls is the subclass
|
302
|
+
super().__init_subclass__(**kwargs)
|
303
|
+
|
304
|
+
for os_method in get_nonprivate_attributes(OSPlugin):
|
305
|
+
if isinstance(os_method, property):
|
306
|
+
os_method = os_method.fget
|
307
|
+
os_docstring = os_method.__doc__
|
308
|
+
|
309
|
+
method = getattr(cls, os_method.__name__, None)
|
310
|
+
if isinstance(method, property):
|
311
|
+
method = method.fget
|
312
|
+
# This works as None has a __doc__ property (which is None).
|
313
|
+
docstring = method.__doc__
|
314
|
+
|
315
|
+
if method and not docstring:
|
316
|
+
if hasattr(method, "__func__"):
|
317
|
+
method = method.__func__
|
318
|
+
method.__doc__ = os_docstring
|
319
|
+
|
299
320
|
def check_compatible(self) -> bool:
|
300
|
-
"""OSPlugin's use a different compatibility check, override the
|
321
|
+
"""OSPlugin's use a different compatibility check, override the one from the :class:`Plugin` class.
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
This function always returns ``True``.
|
325
|
+
"""
|
301
326
|
return True
|
302
327
|
|
303
328
|
@classmethod
|
304
329
|
def detect(cls, fs: Filesystem) -> Optional[Filesystem]:
|
305
330
|
"""Provide detection of this OSPlugin on a given filesystem.
|
306
331
|
|
307
|
-
Note: must be implemented as a classmethod.
|
308
|
-
|
309
332
|
Args:
|
310
333
|
fs: :class:`~dissect.target.filesystem.Filesystem` to detect the OS on.
|
311
334
|
|
@@ -318,11 +341,9 @@ class OSPlugin(Plugin):
|
|
318
341
|
def create(cls, target: Target, sysvol: Filesystem) -> OSPlugin:
|
319
342
|
"""Initiate this OSPlugin with the given target and detected filesystem.
|
320
343
|
|
321
|
-
Note: must be implemented as a classmethod.
|
322
|
-
|
323
344
|
Args:
|
324
|
-
target: The Target object.
|
325
|
-
sysvol: The filesystem that was detected in the detect() function.
|
345
|
+
target: The :class:`~dissect.target.target.Target` object.
|
346
|
+
sysvol: The filesystem that was detected in the ``detect()`` function.
|
326
347
|
|
327
348
|
Returns:
|
328
349
|
An instantiated version of the OSPlugin.
|
@@ -331,9 +352,7 @@ class OSPlugin(Plugin):
|
|
331
352
|
|
332
353
|
@export(property=True)
|
333
354
|
def hostname(self) -> Optional[str]:
|
334
|
-
"""
|
335
|
-
|
336
|
-
Implementations must be decorated with ``@export(property=True)``.
|
355
|
+
"""Return the target's hostname.
|
337
356
|
|
338
357
|
Returns:
|
339
358
|
The hostname as string.
|
@@ -342,9 +361,7 @@ class OSPlugin(Plugin):
|
|
342
361
|
|
343
362
|
@export(property=True)
|
344
363
|
def ips(self) -> list[str]:
|
345
|
-
"""
|
346
|
-
|
347
|
-
Implementations must be decorated with ``@export(property=True)``.
|
364
|
+
"""Return the IP addresses configured in the target.
|
348
365
|
|
349
366
|
Returns:
|
350
367
|
The IPs as list.
|
@@ -353,9 +370,7 @@ class OSPlugin(Plugin):
|
|
353
370
|
|
354
371
|
@export(property=True)
|
355
372
|
def version(self) -> Optional[str]:
|
356
|
-
"""
|
357
|
-
|
358
|
-
Implementations must be decorated with ``@export(property=True)``.
|
373
|
+
"""Return the target's OS version.
|
359
374
|
|
360
375
|
Returns:
|
361
376
|
The OS version as string.
|
@@ -364,9 +379,7 @@ class OSPlugin(Plugin):
|
|
364
379
|
|
365
380
|
@export(record=EmptyRecord)
|
366
381
|
def users(self) -> list[Record]:
|
367
|
-
"""
|
368
|
-
|
369
|
-
Implementations must be decorated with @export.
|
382
|
+
"""Return the users available in the target.
|
370
383
|
|
371
384
|
Returns:
|
372
385
|
A list of user records.
|
@@ -375,9 +388,7 @@ class OSPlugin(Plugin):
|
|
375
388
|
|
376
389
|
@export(property=True)
|
377
390
|
def os(self) -> str:
|
378
|
-
"""
|
379
|
-
|
380
|
-
Implementations must be decorated with ``@export(property=True)``.
|
391
|
+
"""Return a slug of the target's OS name.
|
381
392
|
|
382
393
|
Returns:
|
383
394
|
A slug of the OS name, e.g. 'windows' or 'linux'.
|
@@ -386,9 +397,7 @@ class OSPlugin(Plugin):
|
|
386
397
|
|
387
398
|
@export(property=True)
|
388
399
|
def architecture(self) -> Optional[str]:
|
389
|
-
"""
|
390
|
-
|
391
|
-
Implementations must be decorated with ``@export(property=True)``.
|
400
|
+
"""Return a slug of the target's OS architecture.
|
392
401
|
|
393
402
|
Returns:
|
394
403
|
A slug of the OS architecture, e.g. 'x86_32-unix', 'MIPS-linux' or
|
@@ -472,16 +481,8 @@ def register(plugincls: Type[Plugin]) -> None:
|
|
472
481
|
elif issubclass(plugincls, ChildTargetPlugin):
|
473
482
|
special_key = "_child"
|
474
483
|
|
475
|
-
|
476
|
-
|
477
|
-
else:
|
478
|
-
plugins = [obj for obj in root[special_key] if obj["class"] == plugincls.__name__]
|
479
|
-
if len(plugins):
|
480
|
-
return
|
481
|
-
|
482
|
-
special_root = {}
|
483
|
-
root[special_key].append(special_root)
|
484
|
-
root = special_root
|
484
|
+
root[special_key] = {}
|
485
|
+
root = root[special_key]
|
485
486
|
|
486
487
|
# Check if the plugin was already registered
|
487
488
|
if "class" in root and root["class"] == plugincls.__name__:
|
@@ -494,6 +495,7 @@ def register(plugincls: Type[Plugin]) -> None:
|
|
494
495
|
root["exports"] = plugincls.__exports__
|
495
496
|
root["namespace"] = plugincls.__namespace__
|
496
497
|
root["fullname"] = ".".join((plugincls.__module__, plugincls.__qualname__))
|
498
|
+
root["is_osplugin"] = issubclass(plugincls, OSPlugin)
|
497
499
|
|
498
500
|
|
499
501
|
def internal(*args, **kwargs) -> Callable:
|
@@ -540,74 +542,166 @@ def arg(*args, **kwargs) -> Callable:
|
|
540
542
|
return decorator
|
541
543
|
|
542
544
|
|
543
|
-
def plugins(
|
544
|
-
|
545
|
+
def plugins(
|
546
|
+
osfilter: Optional[type[OSPlugin]] = None,
|
547
|
+
special_keys: set[str] = set(),
|
548
|
+
only_special_keys: bool = False,
|
549
|
+
) -> Iterator[PluginDescriptor]:
|
550
|
+
"""Walk the ``PLUGINS`` tree and return plugins.
|
545
551
|
|
546
|
-
|
547
|
-
|
552
|
+
If ``osfilter`` is specified, only plugins related to the provided
|
553
|
+
OSPlugin, or plugins with no OS relation are returned.
|
554
|
+
If ``osfilter`` is ``None``, all plugins will be returned.
|
548
555
|
|
549
|
-
|
550
|
-
|
551
|
-
|
556
|
+
One exception to this is if the ``osfilter`` is a (sub-)class of
|
557
|
+
DefaultPlugin, then plugins are returned as if no ``osfilter`` was
|
558
|
+
specified.
|
552
559
|
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
continue
|
560
|
+
Another exeption to this are plugins in the ``PLUGINS`` tree which are
|
561
|
+
under a key that starts with a '_'. Those are only returned if their exact
|
562
|
+
key is specified in ``special_keys``.
|
557
563
|
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
if osfilter and obj["module"].startswith("os") and not obj["module"].startswith(osfilter):
|
563
|
-
continue
|
564
|
+
An exception to these exceptions is in the case of ``OSPlugin`` (sub-)class
|
565
|
+
plugins and ``os_filter`` is not ``None``. These plugins live in the
|
566
|
+
``PLUGINS`` tree under the ``_os`` special key. Those plugins are only
|
567
|
+
returned if they fully match the provided ``osfilter``.
|
564
568
|
|
565
|
-
|
569
|
+
The ``only_special_keys`` option returns only the plugins which are under a
|
570
|
+
special key that is defined in ``special_keys``. All filtering here will
|
571
|
+
happen as stated in the above cases.
|
566
572
|
|
567
|
-
|
568
|
-
osfilter
|
569
|
-
|
570
|
-
and
|
571
|
-
and not issubclass(osfilter, general.default.DefaultPlugin)
|
572
|
-
and osfilter.__module__.startswith(MODULE_PATH)
|
573
|
-
):
|
574
|
-
osfilter, _, _ = osfilter.__module__.replace(MODULE_PATH, "", 1).strip(".").rpartition(".")
|
575
|
-
|
576
|
-
# Continue walking up the OS filter tree until we hit the second level
|
577
|
-
# As an example, it walk os.unix.debian, followed by os.unix, then exit
|
578
|
-
os_parts = osfilter.split(".")
|
579
|
-
while len(os_parts) >= 2:
|
580
|
-
yield from _walk(".".join(os_parts), _get_plugins())
|
581
|
-
os_parts.pop()
|
582
|
-
else:
|
583
|
-
yield from _walk(None, _get_plugins())
|
573
|
+
Args:
|
574
|
+
osfilter: The optional OSPlugin to filter the returned plugins on.
|
575
|
+
special_keys: Also return plugins which are under the special ('_') keys in this set.
|
576
|
+
only_special_keys: Only return the plugins under the keys in ``special_keys`` and no others.
|
584
577
|
|
578
|
+
Yields:
|
579
|
+
Plugins in the ``PLUGINS`` tree based on the given filter criteria.
|
580
|
+
"""
|
585
581
|
|
586
|
-
|
587
|
-
|
582
|
+
if osfilter is not None:
|
583
|
+
# The PLUGINS tree does not include the hierarchy up to the plugins
|
584
|
+
# directory (dissect.target.plugins) for the built-in plugins. For the
|
585
|
+
# plugins in the directory specified in --plugin-path, the hierarchy
|
586
|
+
# starts at that directory.
|
587
|
+
#
|
588
|
+
# Built-in OSPlugins do have the dissect.target.plugins path in their
|
589
|
+
# module name, so it needs to be stripped, e.g.:
|
590
|
+
# dissect.target.plugins.general.default -> general.default
|
591
|
+
# dissect.target.plugins.windows._os -> plugins.windows._os
|
592
|
+
#
|
593
|
+
# The module name of OSPlugins from --plugin-path starts at the
|
594
|
+
# directory specified in that option, e.g.:
|
595
|
+
# --plugin-path=/some/path/, with a file foo/baros/_os.py
|
596
|
+
# will have a module name of: foo.baros._os
|
597
|
+
filter_path = _modulepath(osfilter).split(".")
|
598
|
+
|
599
|
+
# If an OSPlugin is not defined in a file called _os.py, an extra `_os`
|
600
|
+
# part is added to the PLUGINS tree.
|
601
|
+
# For example the default OS plugin with module name general.default
|
602
|
+
# (after stripping of the build-in hierarchy) will be added at:
|
603
|
+
# general
|
604
|
+
# \- default
|
605
|
+
# \- _os
|
606
|
+
# However the `_os` part is not in the module name. Modules that are
|
607
|
+
# defined in an _os.py file have the `_os` part in their module name.
|
608
|
+
# It is stripped out, so the filter is similar for both types of
|
609
|
+
# OSPlugin files.
|
610
|
+
if filter_path[-1] == "_os":
|
611
|
+
filter_path = filter_path[:-1]
|
612
|
+
else:
|
613
|
+
filter_path = []
|
588
614
|
|
589
|
-
def _walk(
|
615
|
+
def _walk(
|
616
|
+
root: dict,
|
617
|
+
special_keys: set[str] = set(),
|
618
|
+
only_special_keys: bool = False,
|
619
|
+
prev_module_path: list[str] = [],
|
620
|
+
):
|
590
621
|
for key, obj in root.items():
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
622
|
+
module_path = prev_module_path.copy()
|
623
|
+
module_path.append(key)
|
624
|
+
|
625
|
+
# A branch in the PLUGINS tree is traversed to the next level if:
|
626
|
+
# - there are no filters (which in effect means all plugins are
|
627
|
+
# returned including all _os plugins).
|
628
|
+
# - the osfilter is the default plugin (which means all normal plugins but
|
629
|
+
# only the default _os plugin is returned).
|
630
|
+
# - there is no _os plugin on the next level (we're traversing a
|
631
|
+
# "normal" plugin branch or already jumped into an OS specific
|
632
|
+
# branch because of a filter_path match)
|
633
|
+
# - the current module_path fully matches the (beginning of) the
|
634
|
+
# filter path (this allows traversing into the specific os branch
|
635
|
+
# for the given os filter and any sub branches which are not os
|
636
|
+
# branches (of a sub-os) themselves).
|
637
|
+
if (
|
638
|
+
not filter_path
|
639
|
+
or issubclass(osfilter, general.default.DefaultPlugin)
|
640
|
+
or "_os" not in obj
|
641
|
+
or module_path == filter_path[: len(module_path)]
|
642
|
+
):
|
643
|
+
if key.startswith("_"):
|
644
|
+
if key in special_keys:
|
645
|
+
# OSPlugins are treated special and are only returned
|
646
|
+
# if their module_path matches the full filter_path.
|
647
|
+
#
|
648
|
+
# Note that the module_path includes the `_os` part,
|
649
|
+
# which may have been explicitly added in the
|
650
|
+
# hierarchy. This part needs to be stripped out when
|
651
|
+
# matching against the filter_path, where it was either
|
652
|
+
# not present or stripped out.
|
653
|
+
if key != "_os" or (
|
654
|
+
key == "_os" and (not filter_path or (filter_path and module_path[:-1] == filter_path))
|
655
|
+
):
|
656
|
+
# If the special key is a leaf-node, we just give it back.
|
657
|
+
# If it is a branch, we give back the full branch,
|
658
|
+
# not just the special_keys if only_special_keys
|
659
|
+
# was set to True.
|
660
|
+
if "functions" in obj:
|
661
|
+
yield obj
|
662
|
+
else:
|
663
|
+
yield from _walk(
|
664
|
+
obj,
|
665
|
+
special_keys=special_keys,
|
666
|
+
only_special_keys=False,
|
667
|
+
prev_module_path=module_path,
|
668
|
+
)
|
669
|
+
else:
|
670
|
+
continue
|
671
|
+
else:
|
672
|
+
continue
|
599
673
|
|
600
|
-
|
674
|
+
else:
|
675
|
+
if "functions" in obj:
|
676
|
+
if not (special_keys and only_special_keys):
|
677
|
+
yield obj
|
678
|
+
else:
|
679
|
+
yield from _walk(
|
680
|
+
obj,
|
681
|
+
special_keys=special_keys,
|
682
|
+
only_special_keys=only_special_keys,
|
683
|
+
prev_module_path=module_path,
|
684
|
+
)
|
685
|
+
|
686
|
+
yield from sorted(
|
687
|
+
_walk(
|
688
|
+
_get_plugins(),
|
689
|
+
special_keys=special_keys,
|
690
|
+
only_special_keys=only_special_keys,
|
691
|
+
),
|
692
|
+
key=lambda plugin: len(plugin["module"]),
|
693
|
+
reverse=True,
|
694
|
+
)
|
601
695
|
|
602
696
|
|
603
697
|
def os_plugins() -> Iterator[PluginDescriptor]:
|
604
698
|
"""Retrieve all OS plugin descriptors."""
|
605
|
-
yield from
|
699
|
+
yield from plugins(special_keys={"_os"}, only_special_keys=True)
|
606
700
|
|
607
701
|
|
608
702
|
def child_plugins() -> Iterator[PluginDescriptor]:
|
609
703
|
"""Retrieve all child plugin descriptors."""
|
610
|
-
yield from
|
704
|
+
yield from plugins(special_keys={"_child"}, only_special_keys=True)
|
611
705
|
|
612
706
|
|
613
707
|
def lookup(func_name: str, osfilter: Optional[type[OSPlugin]] = None) -> Iterator[PluginDescriptor]:
|
@@ -645,7 +739,7 @@ def get_plugins_by_namespace(namespace: str, osfilter: Optional[type[OSPlugin]]
|
|
645
739
|
yield plugin_desc
|
646
740
|
|
647
741
|
|
648
|
-
def load(plugin_desc:
|
742
|
+
def load(plugin_desc: PluginDescriptor) -> Type[Plugin]:
|
649
743
|
"""Helper function that loads a plugin from a given plugin description.
|
650
744
|
|
651
745
|
Args:
|
@@ -729,7 +823,7 @@ def load_module_from_name(module_path: str) -> None:
|
|
729
823
|
# This will trigger the __init__subclass__() of the Plugin subclasses in the module.
|
730
824
|
importlib.import_module(module_path)
|
731
825
|
except Exception as e:
|
732
|
-
log.
|
826
|
+
log.info("Unable to import %s", module_path)
|
733
827
|
log.debug("Error while trying to import module %s", module_path, exc_info=e)
|
734
828
|
save_plugin_import_failure(module_path)
|
735
829
|
|
@@ -815,7 +909,8 @@ def _traverse(key: str, obj: dict[str, Any]) -> dict[str, Any]:
|
|
815
909
|
|
816
910
|
def _modulepath(cls) -> str:
|
817
911
|
"""Returns the module path of a :class:`Plugin` relative to ``dissect.target.plugins``."""
|
818
|
-
|
912
|
+
module = getattr(cls, "__module__", "")
|
913
|
+
return module.replace(MODULE_PATH, "").lstrip(".")
|
819
914
|
|
820
915
|
|
821
916
|
# These need to be at the bottom of the module because __init_subclass__ requires everything
|
@@ -860,6 +955,10 @@ class NamespacePlugin(Plugin):
|
|
860
955
|
# the direct subclass of NamespacePlugin
|
861
956
|
cls.__nsplugin__.SUBPLUGINS.add(cls.__namespace__)
|
862
957
|
|
958
|
+
# Generate a tuple of class names for which we do not want to add subplugin functions, which is the
|
959
|
+
# namespaceplugin and all of its superclasses (minus the base object).
|
960
|
+
reserved_cls_names = tuple({_class.__name__ for _class in cls.__nsplugin__.mro() if _class is not object})
|
961
|
+
|
863
962
|
# Collect the public attrs of the subplugin
|
864
963
|
for subplugin_func_name in cls.__exports__:
|
865
964
|
subplugin_func = inspect.getattr_static(cls, subplugin_func_name)
|
@@ -872,12 +971,15 @@ class NamespacePlugin(Plugin):
|
|
872
971
|
if getattr(subplugin_func, "__output__", None) != "record":
|
873
972
|
continue
|
874
973
|
|
875
|
-
# The method
|
876
|
-
if
|
974
|
+
# The method may not be part of a parent class.
|
975
|
+
if subplugin_func.__qualname__.startswith(reserved_cls_names):
|
877
976
|
continue
|
878
977
|
|
879
978
|
# If we already have an aggregate method, skip
|
880
979
|
if existing_aggregator := getattr(cls.__nsplugin__, subplugin_func_name, None):
|
980
|
+
if not hasattr(existing_aggregator, "__subplugins__"):
|
981
|
+
# This is not an aggregator, but a re-implementation of a subclass function by the subplugin.
|
982
|
+
continue
|
881
983
|
existing_aggregator.__subplugins__.append(cls.__namespace__)
|
882
984
|
continue
|
883
985
|
|
@@ -887,10 +989,12 @@ class NamespacePlugin(Plugin):
|
|
887
989
|
for entry in aggregator.__subplugins__:
|
888
990
|
try:
|
889
991
|
subplugin = getattr(self.target, entry)
|
890
|
-
|
891
|
-
|
892
|
-
except Exception:
|
992
|
+
yield from getattr(subplugin, method_name)()
|
993
|
+
except UnsupportedPluginError:
|
893
994
|
continue
|
995
|
+
except Exception as e:
|
996
|
+
self.target.log.error("Subplugin: %s raised an exception for: %s", entry, method_name)
|
997
|
+
self.target.log.debug("Exception: %s", e, exc_info=e)
|
894
998
|
|
895
999
|
# Holds the subplugins that share this method
|
896
1000
|
aggregator.__subplugins__ = []
|
@@ -973,54 +1077,65 @@ class PluginFunction:
|
|
973
1077
|
output_type: str
|
974
1078
|
class_object: type[Plugin]
|
975
1079
|
method_name: str
|
976
|
-
plugin_desc:
|
1080
|
+
plugin_desc: PluginDescriptor = field(hash=False)
|
977
1081
|
|
978
1082
|
|
979
|
-
def plugin_function_index(target: Target) -> tuple[dict[str,
|
1083
|
+
def plugin_function_index(target: Optional[Target]) -> tuple[dict[str, PluginDescriptor], set[str]]:
|
980
1084
|
"""Returns an index-list for plugins.
|
981
1085
|
|
982
1086
|
This list is used to match CLI expressions against to find the desired plugin.
|
983
1087
|
Also returns the roots to determine whether a CLI expression has to be compared
|
984
1088
|
to the plugin tree or parsed using legacy rules.
|
985
1089
|
"""
|
1090
|
+
|
1091
|
+
if target is None:
|
1092
|
+
os_type = None
|
1093
|
+
elif target._os_plugin is None:
|
1094
|
+
os_type = general.default.DefaultPlugin
|
1095
|
+
elif isinstance(target._os_plugin, type) and issubclass(target._os_plugin, OSPlugin):
|
1096
|
+
os_type = target._os_plugin
|
1097
|
+
elif isinstance(target._os_plugin, OSPlugin):
|
1098
|
+
os_type = type(target._os_plugin)
|
1099
|
+
else:
|
1100
|
+
raise TypeError(
|
1101
|
+
"target must be None or target._os_plugin must be either None, "
|
1102
|
+
"a subclass of OSPlugin or an instance of OSPlugin"
|
1103
|
+
)
|
1104
|
+
|
986
1105
|
index = {}
|
987
1106
|
rootset = set()
|
988
1107
|
|
989
|
-
|
990
|
-
# Filter out plugins based on the target os
|
991
|
-
os_type = type(target._os) if target._os and target._os.os != "default" else None
|
1108
|
+
all_plugins = plugins(osfilter=os_type, special_keys={"_child", "_os"})
|
992
1109
|
|
993
|
-
|
994
|
-
yield from os_plugins()
|
995
|
-
yield from child_plugins() # Doesn't export anything but added for completeness.
|
996
|
-
|
997
|
-
for available_original in all_plugins():
|
1110
|
+
for available_original in all_plugins:
|
998
1111
|
# Prevent modifying the global PLUGINS dict, otherwise -f os.windows._os.users fails for instance.
|
999
1112
|
available = available_original.copy()
|
1113
|
+
|
1114
|
+
modulepath = available["module"]
|
1115
|
+
rootset.add(modulepath.split(".")[0])
|
1116
|
+
|
1000
1117
|
if "get_all_records" in available["exports"]:
|
1118
|
+
# The get_all_records does not only need to be not present in the
|
1119
|
+
# index, it also needs to be removed from the exports list, else
|
1120
|
+
# the 'plugins' plugin will still display them.
|
1001
1121
|
available["exports"].remove("get_all_records")
|
1002
|
-
modulepath = available["module"]
|
1003
1122
|
|
1004
|
-
|
1005
|
-
if
|
1006
|
-
#
|
1123
|
+
for exported in available["exports"]:
|
1124
|
+
if available["is_osplugin"] and os_type == general.default.DefaultPlugin:
|
1125
|
+
# This makes the os plugin exports listed under the special
|
1126
|
+
# "OS plugins" header by the 'plugins' plugin.
|
1007
1127
|
available["module"] = ""
|
1008
|
-
|
1009
|
-
|
1010
|
-
rootset.add(modulepath.split(".")[0])
|
1011
|
-
for exported in available["exports"]:
|
1012
|
-
index[f"{modulepath}.{exported}"] = available
|
1013
|
-
index[exported] = available
|
1014
|
-
else:
|
1015
|
-
rootset.add(modulepath.split(".")[0])
|
1016
|
-
for exported in available["exports"]:
|
1017
|
-
index[f"{modulepath}.{exported}"] = available
|
1128
|
+
|
1129
|
+
index[f"{modulepath}.{exported}"] = available
|
1018
1130
|
|
1019
1131
|
return index, rootset
|
1020
1132
|
|
1021
1133
|
|
1022
1134
|
def find_plugin_functions(
|
1023
|
-
target: Target,
|
1135
|
+
target: Optional[Target],
|
1136
|
+
patterns: str,
|
1137
|
+
compatibility: bool = False,
|
1138
|
+
**kwargs,
|
1024
1139
|
) -> tuple[list[PluginFunction], set[str]]:
|
1025
1140
|
"""Finds plugins that match the target and the patterns.
|
1026
1141
|
|
@@ -1030,10 +1145,6 @@ def find_plugin_functions(
|
|
1030
1145
|
"""
|
1031
1146
|
result = []
|
1032
1147
|
|
1033
|
-
def add_to_result(func: PluginFunction) -> None:
|
1034
|
-
if func not in result and not func.class_object.__skip__:
|
1035
|
-
result.append(func)
|
1036
|
-
|
1037
1148
|
functions, rootset = plugin_function_index(target)
|
1038
1149
|
|
1039
1150
|
invalid_funcs = set()
|
@@ -1041,29 +1152,45 @@ def find_plugin_functions(
|
|
1041
1152
|
ignore_load_errors = kwargs.get("ignore_load_errors", False)
|
1042
1153
|
|
1043
1154
|
for pattern in patterns.split(","):
|
1044
|
-
#
|
1155
|
+
# Backward compatibility fix for namespace-level plugins (i.e. chrome)
|
1156
|
+
# If an exact namespace match is found, the pattern is changed to the tree to that namespace.
|
1157
|
+
# Examples:
|
1158
|
+
# -f browser -> apps.browser.browser
|
1159
|
+
# -f iexplore -> apps.browser.iexplore
|
1160
|
+
namespace_match = False
|
1045
1161
|
for index_name, func in functions.items():
|
1046
1162
|
if func["namespace"] == pattern:
|
1047
|
-
pattern = func["module"]
|
1163
|
+
pattern = func["module"]
|
1164
|
+
namespace_match = True
|
1165
|
+
break
|
1048
1166
|
|
1049
1167
|
wildcard = any(char in pattern for char in ["*", "!", "?", "[", "]"])
|
1050
1168
|
treematch = pattern.split(".")[0] in rootset and pattern != "os"
|
1051
1169
|
exact_match = pattern in functions
|
1052
1170
|
|
1053
|
-
# Allow for exact matches
|
1054
|
-
#
|
1055
|
-
#
|
1056
|
-
#
|
1057
|
-
#
|
1058
|
-
|
1171
|
+
# Allow for exact and namespace matches even if the plugin does not want to be found, otherwise you cannot
|
1172
|
+
# reach documented namespace plugins like apps.browser.browser.downloads.
|
1173
|
+
# You can *always* run these using the namespace/classic-style like: browser.downloads (but -l lists them
|
1174
|
+
# in the tree for documentation purposes so it would be misleading not to allow tree access as well).
|
1175
|
+
#
|
1176
|
+
# Note that these tree items will never respond to wildcards though to avoid duplicate results, e.g. when
|
1177
|
+
# querying apps.browser.*, this also means apps.browser.browser.* won't work.
|
1178
|
+
if exact_match or namespace_match:
|
1059
1179
|
show_hidden = True
|
1060
1180
|
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1181
|
+
# Change the treematch pattern into an fnmatch-able pattern to give back all functions from the sub-tree
|
1182
|
+
# (if there is a subtree).
|
1183
|
+
#
|
1184
|
+
# Examples:
|
1185
|
+
# -f browser -> apps.browser.browser* (the whole package, due to a namespace match)
|
1186
|
+
# -f apps.webservers.iis -> apps.webservers.iis* (logs etc)
|
1187
|
+
# -f apps.webservers.iis.logs -> apps.webservers.iis.logs* (only the logs, there is no subtree)
|
1188
|
+
# We do not include a dot because that does not work if the full path is given:
|
1189
|
+
# -f apps.webservers.iis.logs != apps.webservers.iis.logs.* (does not work)
|
1190
|
+
#
|
1191
|
+
# In practice a namespace_match would almost always also be a treematch, except when the namespace plugin
|
1192
|
+
# is in the root of the plugin tree.
|
1193
|
+
if (treematch or namespace_match) and not wildcard and not exact_match:
|
1067
1194
|
pattern += "*"
|
1068
1195
|
|
1069
1196
|
if wildcard or treematch:
|
@@ -1086,6 +1213,8 @@ def find_plugin_functions(
|
|
1086
1213
|
fobject = inspect.getattr_static(loaded_plugin_object, method_name)
|
1087
1214
|
|
1088
1215
|
if compatibility:
|
1216
|
+
if target is None:
|
1217
|
+
continue
|
1089
1218
|
try:
|
1090
1219
|
if not loaded_plugin_object(target).is_compatible():
|
1091
1220
|
continue
|
@@ -1093,7 +1222,7 @@ def find_plugin_functions(
|
|
1093
1222
|
continue
|
1094
1223
|
|
1095
1224
|
matches = True
|
1096
|
-
|
1225
|
+
result.append(
|
1097
1226
|
PluginFunction(
|
1098
1227
|
name=f"{func['namespace']}.{method_name}" if func["namespace"] else method_name,
|
1099
1228
|
path=index_name,
|
@@ -1116,9 +1245,9 @@ def find_plugin_functions(
|
|
1116
1245
|
namespace = None
|
1117
1246
|
|
1118
1247
|
plugin_descriptions = []
|
1119
|
-
for
|
1120
|
-
nsmatch = namespace and func["namespace"] == namespace and
|
1121
|
-
fmatch = not namespace and not func["namespace"] and
|
1248
|
+
for func_path, func in functions.items():
|
1249
|
+
nsmatch = namespace and func["namespace"] == namespace and func_path.split(".")[-1] == funcname
|
1250
|
+
fmatch = not namespace and not func["namespace"] and func_path.split(".")[-1] == funcname
|
1122
1251
|
if nsmatch or fmatch:
|
1123
1252
|
plugin_descriptions.append(func)
|
1124
1253
|
|
@@ -1138,7 +1267,7 @@ def find_plugin_functions(
|
|
1138
1267
|
if compatibility and not loaded_plugin_object(target).is_compatible():
|
1139
1268
|
continue
|
1140
1269
|
|
1141
|
-
|
1270
|
+
result.append(
|
1142
1271
|
PluginFunction(
|
1143
1272
|
name=f"{description['namespace']}.{funcname}" if description["namespace"] else funcname,
|
1144
1273
|
path=f"{description['module']}.{funcname}",
|