From 794d4ae0e52acf80c3bd5dec1589e9b7415e6088 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:42:26 +0200 Subject: [PATCH 1/6] add unix trash plugin --- dissect/target/plugins/os/unix/trash.py | 86 +++++++++++++++++++++++++ tests/plugins/os/unix/test_trash.py | 0 2 files changed, 86 insertions(+) create mode 100644 dissect/target/plugins/os/unix/trash.py create mode 100644 tests/plugins/os/unix/test_trash.py diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py new file mode 100644 index 000000000..bf199aa6f --- /dev/null +++ b/dissect/target/plugins/os/unix/trash.py @@ -0,0 +1,86 @@ +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers import configutil +from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension +from dissect.target.helpers.fsutil import TargetPath +from dissect.target.helpers.record import create_extended_descriptor +from dissect.target.plugin import Plugin, alias, export +from dissect.target.plugins.general.users import UserDetails +from dissect.target.target import Target + +TrashRecord = create_extended_descriptor([UserRecordDescriptorExtension])( + "linux/filesystem/recyclebin", + [ + ("datetime", "ts"), + ("path", "path"), + ("filesize", "filesize"), + ("path", "deleted_path"), + ("path", "source"), + ], +) + + +class GnomeTrashPlugin(Plugin): + """Linux GNOME Trash plugin. + + Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``. + Probably also works with other desktop interfaces as long as they follow + the Trash specification from FreeDesktop. + + Resources: + - https://specifications.freedesktop.org/trash-spec/latest/ + - https://github.com/GNOME/glib/blob/main/gio/glocalfile.c + - https://specifications.freedesktop.org/basedir-spec/latest/ + """ + + PATHS = [ + # Default $XDG_DATA_HOME/Trash + ".local/share/Trash", + ] + + def __init__(self, target: Target): + super().__init__(target) + self.trashes = list(self.garbage_collector()) + + def garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]: + for user_details in self.target.user_details.all_with_home(): + for trash_path in self.PATHS: + if (path := user_details.home_path.joinpath(trash_path)).exists(): + yield user_details, path + + def check_compatible(self) -> None: + if not self.trashes: + raise UnsupportedPluginError("No Trash folder(s) found") + + @export(record=TrashRecord) + @alias(name="recyclebin") + def trash(self) -> Iterator[TrashRecord]: + """Yield deleted files from GNOME Trash folders.""" + + for user_details, trash in self.trashes: + for trash_info_file in trash.glob("info/*.trashinfo"): + trash_info = configutil.parse(trash_info_file, hint="ini").get("Trash Info", {}) + original_path = self.target.fs.path(trash_info.get("Path")) + + # TODO: also iterate the expunged folder + # https://gitlab.gnome.org/GNOME/glib/-/issues/1665 + # https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012 + + if (deleted_path := (trash / "files" / original_path.name)).exists(): + deleted_files = [deleted_path] + + if deleted_path.is_dir(): + for child in deleted_path.rglob("*"): + deleted_files.append(child) + + for file in deleted_files: + yield TrashRecord( + ts=trash_info.get("DeletionDate"), + path=original_path, + filesize=file.lstat().st_size if file.is_file() else None, + deleted_path=file, + source=trash_info_file, + _user=user_details.user, + _target=self.target, + ) diff --git a/tests/plugins/os/unix/test_trash.py b/tests/plugins/os/unix/test_trash.py new file mode 100644 index 000000000..e69de29bb From ba6e4f6ae43b446b406f419979864c94146e0972 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:07:24 +0200 Subject: [PATCH 2/6] add tests --- dissect/target/plugins/os/unix/trash.py | 50 +++++++++++-- .../expunged/123456789/some-dir/some-file.txt | 3 + .../os/unix/trash/files/another-file.bin | 3 + .../plugins/os/unix/trash/files/example.jpg | 3 + .../plugins/os/unix/trash/files/file.txt | 3 + .../plugins/os/unix/trash/files/file.txt.2 | 3 + .../some-dir/another-dir/another-file.txt | 3 + .../unix/trash/files/some-dir/some-file.txt | 3 + .../trash/info/another-file.bin.trashinfo | 3 + .../os/unix/trash/info/example.jpg.trashinfo | 3 + .../os/unix/trash/info/file.txt.2.trashinfo | 3 + .../os/unix/trash/info/file.txt.trashinfo | 3 + .../trash/info/missing-file.txt.trashinfo | 3 + .../os/unix/trash/info/some-dir.trashinfo | 3 + tests/plugins/os/unix/test_trash.py | 74 +++++++++++++++++++ 15 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 tests/_data/plugins/os/unix/trash/expunged/123456789/some-dir/some-file.txt create mode 100644 tests/_data/plugins/os/unix/trash/files/another-file.bin create mode 100644 tests/_data/plugins/os/unix/trash/files/example.jpg create mode 100644 tests/_data/plugins/os/unix/trash/files/file.txt create mode 100644 tests/_data/plugins/os/unix/trash/files/file.txt.2 create mode 100644 tests/_data/plugins/os/unix/trash/files/some-dir/another-dir/another-file.txt create mode 100644 tests/_data/plugins/os/unix/trash/files/some-dir/some-file.txt create mode 100644 tests/_data/plugins/os/unix/trash/info/another-file.bin.trashinfo create mode 100644 tests/_data/plugins/os/unix/trash/info/example.jpg.trashinfo create mode 100644 tests/_data/plugins/os/unix/trash/info/file.txt.2.trashinfo create mode 100644 tests/_data/plugins/os/unix/trash/info/file.txt.trashinfo create mode 100644 tests/_data/plugins/os/unix/trash/info/missing-file.txt.trashinfo create mode 100644 tests/_data/plugins/os/unix/trash/info/some-dir.trashinfo diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py index bf199aa6f..d8fd91949 100644 --- a/dissect/target/plugins/os/unix/trash.py +++ b/dissect/target/plugins/os/unix/trash.py @@ -28,6 +28,8 @@ class GnomeTrashPlugin(Plugin): Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop. + Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``. + Resources: - https://specifications.freedesktop.org/trash-spec/latest/ - https://github.com/GNOME/glib/blob/main/gio/glocalfile.c @@ -61,13 +63,13 @@ def trash(self) -> Iterator[TrashRecord]: for user_details, trash in self.trashes: for trash_info_file in trash.glob("info/*.trashinfo"): trash_info = configutil.parse(trash_info_file, hint="ini").get("Trash Info", {}) - original_path = self.target.fs.path(trash_info.get("Path")) + original_path = self.target.fs.path(trash_info.get("Path", "")) - # TODO: also iterate the expunged folder - # https://gitlab.gnome.org/GNOME/glib/-/issues/1665 - # https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012 + # We use the basename of the .trashinfo file and not the Path variable inside the + # ini file. This way we can keep duplicate basenames of trashed files separated correctly. + deleted_path = trash / "files" / trash_info_file.name.replace(".trashinfo", "") - if (deleted_path := (trash / "files" / original_path.name)).exists(): + if deleted_path.exists(): deleted_files = [deleted_path] if deleted_path.is_dir(): @@ -75,8 +77,12 @@ def trash(self) -> Iterator[TrashRecord]: deleted_files.append(child) for file in deleted_files: + # NOTE: We currently do not 'fix' the original_path of files inside deleted directories. + # This would require guessing where the parent folder starts, which is impossible without + # making assumptions. + yield TrashRecord( - ts=trash_info.get("DeletionDate"), + ts=trash_info.get("DeletionDate", 0), path=original_path, filesize=file.lstat().st_size if file.is_file() else None, deleted_path=file, @@ -84,3 +90,35 @@ def trash(self) -> Iterator[TrashRecord]: _user=user_details.user, _target=self.target, ) + + # We cannot determine if the deleted entry is a directory since the path does + # not exist at $TRASH/files, so we work with what we have instead. + else: + self.target.log.warning(f"Expected trashed file(s) at {deleted_path}") + yield TrashRecord( + ts=trash_info.get("DeletionDate", 0), + path=original_path, + filesize=0, + deleted_path=deleted_path, + source=trash_info_file, + _user=user_details.user, + _target=self.target, + ) + + # We also iterate expunged folders, they can contain files that could not be + # deleted when the user pressed the "empty trash" button in the file manager. + # Resources: + # - https://gitlab.gnome.org/GNOME/glib/-/issues/1665 + # - https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012 + + for item in (trash / "expunged").rglob("*/*"): + stat = item.lstat() + yield TrashRecord( + ts=stat.st_mtime, # NOTE: This is the timestamp at which the file failed to delete + path=None, # We do not know the original file path + filesize=stat.st_size if item.is_file() else None, + deleted_path=item, + source=trash / "expunged", + _user=user_details.user, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/trash/expunged/123456789/some-dir/some-file.txt b/tests/_data/plugins/os/unix/trash/expunged/123456789/some-dir/some-file.txt new file mode 100644 index 000000000..a35ec94ca --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/expunged/123456789/some-dir/some-file.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a63d1c04873abc14c2aaa73d8bcdd202153e969af00cd88e4d38fbe2932c0baa +size 79 diff --git a/tests/_data/plugins/os/unix/trash/files/another-file.bin b/tests/_data/plugins/os/unix/trash/files/another-file.bin new file mode 100644 index 000000000..f0aa9726e --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/another-file.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac49bc57d843413a3b9984a015297282568fc9c6ddbe6477f16abbbbbe51471 +size 369 diff --git a/tests/_data/plugins/os/unix/trash/files/example.jpg b/tests/_data/plugins/os/unix/trash/files/example.jpg new file mode 100644 index 000000000..ad263d05d --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/example.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0806106c60ece2b0fde3ff39f77b51faacd3972c813a418a77d3775384cd97c8 +size 23031 diff --git a/tests/_data/plugins/os/unix/trash/files/file.txt b/tests/_data/plugins/os/unix/trash/files/file.txt new file mode 100644 index 000000000..22254b9a3 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/file.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed645ef0e1abea1bf1e4e935ff04f9e18d39812387f63cda3415b46240f0405 +size 20 diff --git a/tests/_data/plugins/os/unix/trash/files/file.txt.2 b/tests/_data/plugins/os/unix/trash/files/file.txt.2 new file mode 100644 index 000000000..0aabc743f --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/file.txt.2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 +size 10 diff --git a/tests/_data/plugins/os/unix/trash/files/some-dir/another-dir/another-file.txt b/tests/_data/plugins/os/unix/trash/files/some-dir/another-dir/another-file.txt new file mode 100644 index 000000000..77e232208 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/some-dir/another-dir/another-file.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f2d26bce9ba8ad2097db2283241835934c2b507c7d60d90e693ebb4d2622530 +size 22 diff --git a/tests/_data/plugins/os/unix/trash/files/some-dir/some-file.txt b/tests/_data/plugins/os/unix/trash/files/some-dir/some-file.txt new file mode 100644 index 000000000..ea4e7b602 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/files/some-dir/some-file.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ede5152379e915aa3aaf2279a60b82ac4e6f23111e8072f367b29a397f8a6024 +size 23 diff --git a/tests/_data/plugins/os/unix/trash/info/another-file.bin.trashinfo b/tests/_data/plugins/os/unix/trash/info/another-file.bin.trashinfo new file mode 100644 index 000000000..92aaf8bd8 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/another-file.bin.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559bd386869b13f8f376e569013113204fab70f56318f6747e517bba60d23278 +size 103 diff --git a/tests/_data/plugins/os/unix/trash/info/example.jpg.trashinfo b/tests/_data/plugins/os/unix/trash/info/example.jpg.trashinfo new file mode 100644 index 000000000..053c49ae1 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/example.jpg.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:245ec880cab92de5eb315434a921cab48cea2196504eeaf41dfe1163bd7b8f75 +size 83 diff --git a/tests/_data/plugins/os/unix/trash/info/file.txt.2.trashinfo b/tests/_data/plugins/os/unix/trash/info/file.txt.2.trashinfo new file mode 100644 index 000000000..d1fb41420 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/file.txt.2.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78c6153c1acd2674eb2c80210b52c2a54bb3107ecbcb3047cc4f6aee72953a0f +size 79 diff --git a/tests/_data/plugins/os/unix/trash/info/file.txt.trashinfo b/tests/_data/plugins/os/unix/trash/info/file.txt.trashinfo new file mode 100644 index 000000000..8ad8572b2 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/file.txt.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09314dffe9fae3363266902d7b9f75719a79f477f74fd1be21960a0a20129dde +size 81 diff --git a/tests/_data/plugins/os/unix/trash/info/missing-file.txt.trashinfo b/tests/_data/plugins/os/unix/trash/info/missing-file.txt.trashinfo new file mode 100644 index 000000000..9735a798d --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/missing-file.txt.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eab07f3f18a26474bd32316c3581c0c914efa7b7e322340a7c1e1689c2ff65b +size 89 diff --git a/tests/_data/plugins/os/unix/trash/info/some-dir.trashinfo b/tests/_data/plugins/os/unix/trash/info/some-dir.trashinfo new file mode 100644 index 000000000..2fe174757 --- /dev/null +++ b/tests/_data/plugins/os/unix/trash/info/some-dir.trashinfo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6875290b9aefaa75ccd714e91884be61a64cd3e8ffe1b456130f00cc1ce59c8e +size 81 diff --git a/tests/plugins/os/unix/test_trash.py b/tests/plugins/os/unix/test_trash.py index e69de29bb..1aa6edbeb 100644 --- a/tests/plugins/os/unix/test_trash.py +++ b/tests/plugins/os/unix/test_trash.py @@ -0,0 +1,74 @@ +from datetime import datetime, timezone +from io import BytesIO + +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.os.unix._os import UnixPlugin +from dissect.target.plugins.os.unix.trash import GnomeTrashPlugin +from dissect.target.target import Target +from tests._utils import absolute_path + + +def test_gnome_trash(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + """test if GNOME Trash plugin finds all deleted files including recursively deleted folders and expunged items.""" + + fs_unix.map_file_fh("etc/hostname", BytesIO(b"hostname")) + fs_unix.map_dir("home/user/.local/share/Trash", absolute_path("_data/plugins/os/unix/trash")) + target_unix_users.add_plugin(UnixPlugin) + target_unix_users.add_plugin(GnomeTrashPlugin) + + results = sorted(list(target_unix_users.trash()), key=lambda r: r.source) + assert len(results) == 11 + + # test if we find a deleted file + assert results[2].ts == datetime(2024, 12, 31, 13, 37, 0, tzinfo=timezone.utc) + assert results[2].path == "/home/user/Documents/some-location/another-file.bin" + assert results[2].filesize == 369 + assert results[2].deleted_path == "/home/user/.local/share/Trash/files/another-file.bin" + assert results[2].source == "/home/user/.local/share/Trash/info/another-file.bin.trashinfo" + assert results[2].username == "user" + assert results[2].hostname == "hostname" + + # test if we still find a file by just the .trashinfo file and no entry in the $Trash/files folder + assert results[6].path == "/home/user/Downloads/missing-file.txt" + assert results[6].filesize == 0 + assert results[6].source == "/home/user/.local/share/Trash/info/missing-file.txt.trashinfo" + + # test if we find a deleted directory + assert results[7].ts == datetime(2024, 12, 31, 1, 2, 3, tzinfo=timezone.utc) + assert results[7].path == "/home/user/Downloads/some-dir" + assert results[7].filesize is None + assert results[7].deleted_path == "/home/user/.local/share/Trash/files/some-dir" + assert results[7].source == "/home/user/.local/share/Trash/info/some-dir.trashinfo" + assert results[7].username == "user" + assert results[7].hostname == "hostname" + + # test if we find files nested inside a deleted directory + deleted_paths = [r.deleted_path for r in results] + assert deleted_paths == [ + "/home/user/.local/share/Trash/expunged/123456789/some-dir", + "/home/user/.local/share/Trash/expunged/123456789/some-dir/some-file.txt", + "/home/user/.local/share/Trash/files/another-file.bin", + "/home/user/.local/share/Trash/files/example.jpg", + "/home/user/.local/share/Trash/files/file.txt.2", + "/home/user/.local/share/Trash/files/file.txt", + "/home/user/.local/share/Trash/files/missing-file.txt", + "/home/user/.local/share/Trash/files/some-dir", + "/home/user/.local/share/Trash/files/some-dir/another-dir", + "/home/user/.local/share/Trash/files/some-dir/some-file.txt", + "/home/user/.local/share/Trash/files/some-dir/another-dir/another-file.txt", + ] + + # test if we find two deleted files that had the same basename + assert results[4].path == "/home/user/Desktop/file.txt" + assert results[4].deleted_path == "/home/user/.local/share/Trash/files/file.txt.2" + assert results[4].filesize == 10 + assert results[5].path == "/home/user/Documents/file.txt" + assert results[5].deleted_path == "/home/user/.local/share/Trash/files/file.txt" + assert results[5].filesize == 20 + + # test if we find expunged files + assert results[0].path is None + assert results[0].deleted_path == "/home/user/.local/share/Trash/expunged/123456789/some-dir" + assert results[1].path is None + assert results[1].deleted_path == "/home/user/.local/share/Trash/expunged/123456789/some-dir/some-file.txt" + assert results[1].filesize == 79 From c983484708bc2240955b595b734f61c584819d13 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:23:26 +0200 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> --- dissect/target/plugins/os/unix/trash.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py index d8fd91949..5862f825f 100644 --- a/dissect/target/plugins/os/unix/trash.py +++ b/dissect/target/plugins/os/unix/trash.py @@ -25,8 +25,7 @@ class GnomeTrashPlugin(Plugin): """Linux GNOME Trash plugin. Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``. - Probably also works with other desktop interfaces as long as they follow - the Trash specification from FreeDesktop. + Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop. Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``. @@ -80,7 +79,6 @@ def trash(self) -> Iterator[TrashRecord]: # NOTE: We currently do not 'fix' the original_path of files inside deleted directories. # This would require guessing where the parent folder starts, which is impossible without # making assumptions. - yield TrashRecord( ts=trash_info.get("DeletionDate", 0), path=original_path, @@ -110,7 +108,6 @@ def trash(self) -> Iterator[TrashRecord]: # Resources: # - https://gitlab.gnome.org/GNOME/glib/-/issues/1665 # - https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012 - for item in (trash / "expunged").rglob("*/*"): stat = item.lstat() yield TrashRecord( From 48b05f672c3bcbcbe883f323e17614d8952558e2 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:17:07 +0200 Subject: [PATCH 4/6] implement review feedback --- tests/plugins/os/unix/test_trash.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/plugins/os/unix/test_trash.py b/tests/plugins/os/unix/test_trash.py index 1aa6edbeb..7ca2075bd 100644 --- a/tests/plugins/os/unix/test_trash.py +++ b/tests/plugins/os/unix/test_trash.py @@ -16,6 +16,10 @@ def test_gnome_trash(target_unix_users: Target, fs_unix: VirtualFilesystem) -> N target_unix_users.add_plugin(UnixPlugin) target_unix_users.add_plugin(GnomeTrashPlugin) + # test if the plugin and its alias were registered + assert target_unix_users.has_function("trash") + assert target_unix_users.has_function("recyclebin") + results = sorted(list(target_unix_users.trash()), key=lambda r: r.source) assert len(results) == 11 From b5746841a449d1597d068b6d4a891468333ff435 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:21:37 +0200 Subject: [PATCH 5/6] implement review comments --- dissect/target/plugins/os/unix/trash.py | 41 ++++++++++++++++--------- dissect/target/tools/utils.py | 4 +++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py index 5862f825f..e326f7585 100644 --- a/dissect/target/plugins/os/unix/trash.py +++ b/dissect/target/plugins/os/unix/trash.py @@ -22,18 +22,7 @@ class GnomeTrashPlugin(Plugin): - """Linux GNOME Trash plugin. - - Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``. - Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop. - - Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``. - - Resources: - - https://specifications.freedesktop.org/trash-spec/latest/ - - https://github.com/GNOME/glib/blob/main/gio/glocalfile.c - - https://specifications.freedesktop.org/basedir-spec/latest/ - """ + """Linux GNOME Trash plugin.""" PATHS = [ # Default $XDG_DATA_HOME/Trash @@ -42,9 +31,10 @@ class GnomeTrashPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.trashes = list(self.garbage_collector()) + self.trashes = list(self._garbage_collector()) - def garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]: + def _garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]: + """it aint much, but its honest work""" for user_details in self.target.user_details.all_with_home(): for trash_path in self.PATHS: if (path := user_details.home_path.joinpath(trash_path)).exists(): @@ -57,7 +47,28 @@ def check_compatible(self) -> None: @export(record=TrashRecord) @alias(name="recyclebin") def trash(self) -> Iterator[TrashRecord]: - """Yield deleted files from GNOME Trash folders.""" + """Yield deleted files from GNOME Trash folders. + + Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``. + Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop. + + Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``. + + Resources: + - https://specifications.freedesktop.org/trash-spec/latest/ + - https://github.com/GNOME/glib/blob/main/gio/glocalfile.c + - https://specifications.freedesktop.org/basedir-spec/latest/ + + Yields ``TrashRecord``s with the following fields: + + .. code-block:: text + + ts (datetime): timestamp when the file was deleted or for expunged files when it could not be permanently deleted + path (path): path where the file was located before it was deleted + filesize (filesize): size in bytes of the deleted file + deleted_path (path): path to the current location of the deleted file + source (path): path to the .trashinfo file + """ # noqa: E501 for user_details, trash in self.trashes: for trash_info_file in trash.glob("info/*.trashinfo"): diff --git a/dissect/target/tools/utils.py b/dissect/target/tools/utils.py index a27ee8676..38d1c1df1 100644 --- a/dissect/target/tools/utils.py +++ b/dissect/target/tools/utils.py @@ -91,6 +91,10 @@ def generate_argparse_for_unbound_method( raise ValueError(f"Value `{method}` is not an unbound plugin method") desc = method.__doc__ or docs.get_func_description(method, with_docstrings=True) + + if "\n" in desc: + desc = desc.split("\n")[0] + "\n" + textwrap.dedent("\n".join(desc.split("\n")[1:])) + help_formatter = argparse.RawDescriptionHelpFormatter parser = argparse.ArgumentParser(description=desc, formatter_class=help_formatter, conflict_handler="resolve") From cdc3426192dfb8c119a4bc6451a67a9ab79253a6 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:58:10 +0200 Subject: [PATCH 6/6] Update dissect/target/tools/utils.py Co-authored-by: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> --- dissect/target/tools/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/tools/utils.py b/dissect/target/tools/utils.py index 38d1c1df1..14fe3d26a 100644 --- a/dissect/target/tools/utils.py +++ b/dissect/target/tools/utils.py @@ -93,7 +93,7 @@ def generate_argparse_for_unbound_method( desc = method.__doc__ or docs.get_func_description(method, with_docstrings=True) if "\n" in desc: - desc = desc.split("\n")[0] + "\n" + textwrap.dedent("\n".join(desc.split("\n")[1:])) + desc = inspect.cleandoc(desc) help_formatter = argparse.RawDescriptionHelpFormatter parser = argparse.ArgumentParser(description=desc, formatter_class=help_formatter, conflict_handler="resolve")