From 59ac89c7130173024fadeb9799b29070e54d5c70 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:31:27 +0200 Subject: [PATCH 1/8] Fix logging of incompatible plugins --- dissect/target/plugin.py | 7 +++++-- dissect/target/target.py | 8 +++----- dissect/target/tools/query.py | 6 +++--- tests/test_plugin.py | 13 +++++++++---- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/dissect/target/plugin.py b/dissect/target/plugin.py index f02b6171d..3b59b8d9a 100644 --- a/dissect/target/plugin.py +++ b/dissect/target/plugin.py @@ -955,6 +955,7 @@ def __init_subclass__(cls, **kwargs): @dataclass(frozen=True, eq=True) class PluginFunction: name: str + path: str output_type: str class_object: type[Plugin] method_name: str @@ -1074,7 +1075,8 @@ def add_to_result(func: PluginFunction) -> None: matches = True add_to_result( PluginFunction( - name=index_name, + name=f"{func['namespace']}.{method_name}" if func["namespace"] else method_name, + path=index_name, class_object=loaded_plugin_object, method_name=method_name, output_type=getattr(fobject, "__output__", "text"), @@ -1112,7 +1114,8 @@ def add_to_result(func: PluginFunction) -> None: add_to_result( PluginFunction( - name=f"{description['module']}.{pattern}", + name=f"{description['namespace']}.{funcname}" if description["namespace"] else funcname, + path=f"{description['module']}.{pattern}", class_object=loaded_plugin_object, method_name=funcname, output_type=getattr(fobject, "__output__", "text"), diff --git a/dissect/target/target.py b/dissect/target/target.py index d1144dbb7..fccf5f849 100644 --- a/dissect/target/target.py +++ b/dissect/target/target.py @@ -329,8 +329,7 @@ def _load_child_plugins(self) -> None: continue try: - if not child_plugin.is_compatible(): - continue + child_plugin.check_compatible() self._child_plugins[child_plugin.__type__] = child_plugin except PluginError as e: self.log.info("Child plugin reported itself as incompatible: %s (%s)", plugin_desc["class"], e) @@ -517,10 +516,9 @@ def add_plugin( if check_compatible: try: - if not p.is_compatible(): - self.send_event(Event.INCOMPATIBLE_PLUGIN, plugin_cls=plugin_cls) - raise UnsupportedPluginError(f"Plugin reported itself as incompatible: {plugin_cls}") + p.check_compatible() except PluginError: + self.send_event(Event.INCOMPATIBLE_PLUGIN, plugin_cls=plugin_cls) raise except Exception as e: raise UnsupportedPluginError( diff --git a/dissect/target/tools/query.py b/dissect/target/tools/query.py index 03a0fcc80..2fd2559f6 100644 --- a/dissect/target/tools/query.py +++ b/dissect/target/tools/query.py @@ -256,12 +256,12 @@ def main(): ) except UnsupportedPluginError as e: target.log.error( - "Unsupported plugin for `%s`: %s", - func_def, + "Unsupported plugin for %s: %s", + func_def.name, e.root_cause_str(), ) - target.log.debug("", exc_info=e) + target.log.debug("%s", func_def, exc_info=e) continue except PluginNotFoundError: target.log.error("Cannot find plugin `%s`", func_def) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index ea8d4b359..b83b74c7b 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -118,14 +118,16 @@ def test_find_plugin_function_windows(target_win: Target) -> None: found, _ = find_plugin_functions(target_win, "services") assert len(found) == 1 - assert found[0].name == "os.windows.services.services" + assert found[0].name == "services" + assert found[0].path == "os.windows.services.services" def test_find_plugin_function_unix(target_unix: Target) -> None: found, _ = find_plugin_functions(target_unix, "services") assert len(found) == 1 - assert found[0].name == "os.unix.services.services" + assert found[0].name == "services" + assert found[0].path == "os.unix.services.services" TestRecord = create_extended_descriptor([UserRecordDescriptorExtension])( @@ -175,8 +177,11 @@ def test_find_plugin_function_default(target_default: Target) -> None: assert len(found) == 2 names = [item.name for item in found] - assert "os.unix.services.services" in names - assert "os.windows.services.services" in names + assert "services" in names + assert "services" in names + paths = [item.path for item in found] + assert "os.unix.services.services" in paths + assert "os.windows.services.services" in paths @pytest.mark.parametrize( From 0533ebb496f45186eecb7ca7a6e44a3c8a6d721f Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:44:09 +0200 Subject: [PATCH 2/8] Add unit test --- tests/test_plugin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index b83b74c7b..355da29c0 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -5,11 +5,13 @@ import pytest +from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension from dissect.target.helpers.record import create_extended_descriptor from dissect.target.plugin import ( PLUGINS, NamespacePlugin, + Plugin, environment_variable_paths, export, find_plugin_functions, @@ -197,3 +199,13 @@ def test_find_plugin_function_default(target_default: Target) -> None: def test_find_plugin_function_order(target_win: Target, pattern: str) -> None: found = ",".join(reduce(lambda rs, el: rs + [el.method_name], find_plugin_functions(target_win, pattern)[0], [])) assert found == pattern + + +class _TestIncompatiblePlugin(Plugin): + def check_compatible(self): + raise UnsupportedPluginError("My incompatible plugin error") + + +def test_incompatible_plugin(mock_target: Target) -> None: + with pytest.raises(UnsupportedPluginError, match="My incompatible plugin error"): + mock_target.add_plugin(_TestIncompatiblePlugin) From 92b538fb497be1fb8f9930f791eccbc82a29d668 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:48:39 +0200 Subject: [PATCH 3/8] Add unit test for query log --- tests/test_tools_query.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/test_tools_query.py b/tests/test_tools_query.py index 8c9390263..46109cb12 100644 --- a/tests/test_tools_query.py +++ b/tests/test_tools_query.py @@ -5,13 +5,13 @@ from dissect.target.tools.query import main as target_query -def test_target_query_list(capsys, monkeypatch): +def test_target_query_list(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: with monkeypatch.context() as m: m.setattr("sys.argv", ["target-query", "--list"]) with pytest.raises((SystemExit, IndexError, ImportError)): target_query() - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert out.startswith("Available plugins:") assert "Failed to load:\n None\nAvailable loaders:\n" in out @@ -42,7 +42,12 @@ def test_target_query_list(capsys, monkeypatch): ), ], ) -def test_target_query_invalid_functions(capsys, monkeypatch, given_funcs, expected_invalid_funcs): +def test_target_query_invalid_functions( + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, + given_funcs: list[str], + expected_invalid_funcs: list[str], +) -> None: with monkeypatch.context() as m: m.setattr( "sys.argv", @@ -51,7 +56,7 @@ def test_target_query_invalid_functions(capsys, monkeypatch, given_funcs, expect with pytest.raises((SystemExit)): target_query() - out, err = capsys.readouterr() + _, err = capsys.readouterr() assert "target-query: error: argument -f/--function contains invalid plugin(s):" in err @@ -63,3 +68,16 @@ def test_target_query_invalid_functions(capsys, monkeypatch, given_funcs, expect invalid_funcs.sort() assert invalid_funcs == expected_invalid_funcs + + +def test_target_query_unsupported_plugin_log(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr( + "sys.argv", + ["target-query", "-f", "regf", "tests/data/loaders/tar/test-archive-dot-folder.tgz"], + ) + + target_query() + _, err = capsys.readouterr() + + assert "Unsupported plugin for regf: Registry plugin not loaded" in err From 0552d752800e6ec1ab3fc34bc704796015f3ca4b Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:49:38 +0200 Subject: [PATCH 4/8] Add missing return type --- tests/test_tools_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tools_query.py b/tests/test_tools_query.py index 46109cb12..35f8a6c14 100644 --- a/tests/test_tools_query.py +++ b/tests/test_tools_query.py @@ -70,7 +70,7 @@ def test_target_query_invalid_functions( assert invalid_funcs == expected_invalid_funcs -def test_target_query_unsupported_plugin_log(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch): +def test_target_query_unsupported_plugin_log(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: with monkeypatch.context() as m: m.setattr( "sys.argv", From bb42b0872b5f803c8b3d83c82851bfcff7e03376 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 13 Sep 2023 23:48:19 +0200 Subject: [PATCH 5/8] Use unique path --- dissect/target/tools/query.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dissect/target/tools/query.py b/dissect/target/tools/query.py index 2fd2559f6..07c160e99 100644 --- a/dissect/target/tools/query.py +++ b/dissect/target/tools/query.py @@ -156,11 +156,11 @@ def main(): parser.error("can't list compatible plugins for remote targets.") funcs, _ = find_plugin_functions(plugin_target, args.list, True, show_hidden=True) for func in funcs: - collected_plugins[func.name] = func.plugin_desc + collected_plugins[func.path] = func.plugin_desc else: funcs, _ = find_plugin_functions(Target(), args.list, False, show_hidden=True) for func in funcs: - collected_plugins[func.name] = func.plugin_desc + collected_plugins[func.path] = func.plugin_desc # Display in a user friendly manner target = Target() @@ -196,6 +196,7 @@ def main(): # The only scenario that might cause this is with # custom plugins with idiosyncratic output across OS-versions/branches. output_types = set() + # TODO: fix, slow funcs, invalid_funcs = find_plugin_functions(Target(), args.function, False) for func in funcs: From 1b96c22fd258e0f8bfc54ee98059cc8796253e95 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:32:04 +0200 Subject: [PATCH 6/8] Remove TODO --- dissect/target/tools/query.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dissect/target/tools/query.py b/dissect/target/tools/query.py index 07c160e99..caeb6f8e3 100644 --- a/dissect/target/tools/query.py +++ b/dissect/target/tools/query.py @@ -196,7 +196,6 @@ def main(): # The only scenario that might cause this is with # custom plugins with idiosyncratic output across OS-versions/branches. output_types = set() - # TODO: fix, slow funcs, invalid_funcs = find_plugin_functions(Target(), args.function, False) for func in funcs: From 25b8aa6dda8b310874f60bcd99aa4f4a64a48a95 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:35:11 +0200 Subject: [PATCH 7/8] Fix path for namespaced plugins --- tests/test_plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 355da29c0..19faa1617 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -185,6 +185,9 @@ def test_find_plugin_function_default(target_default: Target) -> None: assert "os.unix.services.services" in paths assert "os.windows.services.services" in paths + found, _ = find_plugin_functions(target_default, "mcafee.msc") + assert found[0].path == "apps.av.mcafee.msc" + @pytest.mark.parametrize( "pattern", From a53b4bdb426ed44ae061e230a48c5d4892be1c88 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:37:02 +0200 Subject: [PATCH 8/8] Actually commit fix and not just test --- dissect/target/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugin.py b/dissect/target/plugin.py index 3b59b8d9a..fd9fc08b0 100644 --- a/dissect/target/plugin.py +++ b/dissect/target/plugin.py @@ -1115,7 +1115,7 @@ def add_to_result(func: PluginFunction) -> None: add_to_result( PluginFunction( name=f"{description['namespace']}.{funcname}" if description["namespace"] else funcname, - path=f"{description['module']}.{pattern}", + path=f"{description['module']}.{funcname}", class_object=loaded_plugin_object, method_name=funcname, output_type=getattr(fobject, "__output__", "text"),