dissect.target 3.14.dev29__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 -12
  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 -16
  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.dev29.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
  79. {dissect.target-3.14.dev29.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.dev29.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
  83. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
  84. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
  85. {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
  86. {dissect.target-3.14.dev29.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
- FORTIGATE = "fortigate"
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 default one."""
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
- """Required OS function.
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
- """Required OS function.
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
- """Required OS function.
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
- """Required OS function.
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
- """Required OS function.
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
- """Required OS function.
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
- if special_key not in root:
476
- root[special_key] = []
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(osfilter: Optional[type[OSPlugin]] = None) -> Iterator[PluginDescriptor]:
544
- """Retrieve all plugin descriptors.
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
- Args:
547
- osfilter: The ``OSPlugin`` to use as template to find os specific plugins for.
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
- Returns:
550
- An iterator of all plugin descriptors, optionally filtered on OS.
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
- def _walk(osfilter: str = None, root: dict = None) -> Iterator[PluginDescriptor]:
554
- for key, obj in root.items():
555
- if key.startswith("_"):
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
- if "functions" not in obj:
559
- for plugin_desc in _walk(osfilter, obj):
560
- yield plugin_desc
561
- else:
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
- yield obj
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
- if (
568
- osfilter
569
- and isinstance(osfilter, type)
570
- and issubclass(osfilter, OSPlugin)
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
- def _special_plugins(special_key: str) -> Iterator[PluginDescriptor]:
587
- """Retrieve plugin descriptors stored under ``special_key``."""
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(root=None):
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
- if key == special_key:
592
- yield from obj
593
-
594
- elif key.startswith("_"):
595
- continue
596
-
597
- elif "functions" not in obj:
598
- yield from _walk(obj)
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
- yield from _walk(_get_plugins())
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 _special_plugins("_os")
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 _special_plugins("_child")
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: dict) -> Type[Plugin]:
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.error("Unable to import %s", module_path)
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
- return cls.__module__.replace(MODULE_PATH, "").lstrip(".")
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 needs to be part of the current subclass and not a parent
876
- if not subplugin_func.__qualname__.startswith(cls.__name__):
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
- for item in getattr(subplugin, method_name)():
891
- yield item
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: dict = field(hash=False)
1080
+ plugin_desc: PluginDescriptor = field(hash=False)
977
1081
 
978
1082
 
979
- def plugin_function_index(target: Target) -> tuple[dict[str, Any], set[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
- def all_plugins():
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
- yield from plugins(os_type)
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
- if modulepath.endswith("._os"):
1005
- if not target._os:
1006
- # if no target available add a namespaceless section
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
- elif target._os.__class__.__name__ != available["class"]:
1009
- continue
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, patterns: str, compatibility: bool = False, **kwargs
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
- # backward compatibility fix for namespace-level plugins (i.e. chrome)
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, otherwise you cannot reach documented namespace plugins like
1054
- # browsers.browser.downloads. You can *always* run these using the namespace/classic-style like:
1055
- # browser.downloads (but -l lists them in the tree for documentation purposes so it would be misleading
1056
- # not to allow tree access as well). Note that these tree items will never respond to wildcards though
1057
- # (browsers.browser.* won't work) to avoid duplicate results.
1058
- if exact_match:
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
- if treematch and not wildcard and not exact_match:
1062
- # Examples:
1063
- # -f browsers -> browsers* (the whole package)
1064
- # -f apps.webservers.iis -> apps.webservers.iis* (logs etc)
1065
- # We do not include a dot because that does not work if the full path is given:
1066
- # -f apps.webservers.iis.logs != apps.webservers.iis.logs.* (does not work)
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
- add_to_result(
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 _, func in functions.items():
1120
- nsmatch = namespace and func["namespace"] == namespace and funcname in func["exports"]
1121
- fmatch = not namespace and not func["namespace"] and funcname in func["exports"]
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
- add_to_result(
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}",