From 624af87b427ddc7e69face8e1db9c6b01d61a9f0 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:46:39 +0200 Subject: [PATCH 1/2] add support for trash folders in mounts --- dissect/target/filesystem.py | 2 ++ dissect/target/plugins/os/unix/trash.py | 18 +++++++++++++---- tests/plugins/os/unix/test_trash.py | 27 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index e22c099ec..b22960743 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1300,6 +1300,8 @@ def symlink(self, src: str, dst: str) -> None: class LayerFilesystem(Filesystem): __type__ = "layer" + mounts: dict = {} + def __init__(self, **kwargs): self.layers: list[Filesystem] = [] self.mounts = {} diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py index e326f7585..e1177ccf7 100644 --- a/dissect/target/plugins/os/unix/trash.py +++ b/dissect/target/plugins/os/unix/trash.py @@ -31,15 +31,25 @@ class GnomeTrashPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.trashes = list(self._garbage_collector()) + self.trashes = list(set(self._garbage_collector())) def _garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]: """it aint much, but its honest work""" + + # home trash folders 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 + # mounted devices trash folders + for mount_path in list(self.target.fs.mounts.keys()) + ["/mnt", "/media"]: + if mount_path == "/": + continue + + for mount_trash in self.target.fs.path(mount_path).glob("**/.Trash-*"): + yield None, mount_trash + def check_compatible(self) -> None: if not self.trashes: raise UnsupportedPluginError("No Trash folder(s) found") @@ -96,7 +106,7 @@ def trash(self) -> Iterator[TrashRecord]: filesize=file.lstat().st_size if file.is_file() else None, deleted_path=file, source=trash_info_file, - _user=user_details.user, + _user=user_details.user if user_details else None, _target=self.target, ) @@ -110,7 +120,7 @@ def trash(self) -> Iterator[TrashRecord]: filesize=0, deleted_path=deleted_path, source=trash_info_file, - _user=user_details.user, + _user=user_details.user if user_details else None, _target=self.target, ) @@ -127,6 +137,6 @@ def trash(self) -> Iterator[TrashRecord]: filesize=stat.st_size if item.is_file() else None, deleted_path=item, source=trash / "expunged", - _user=user_details.user, + _user=user_details.user if user_details else None, _target=self.target, ) diff --git a/tests/plugins/os/unix/test_trash.py b/tests/plugins/os/unix/test_trash.py index 7ca2075bd..43781fff3 100644 --- a/tests/plugins/os/unix/test_trash.py +++ b/tests/plugins/os/unix/test_trash.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone from io import BytesIO +from unittest.mock import MagicMock, patch from dissect.target.filesystem import VirtualFilesystem from dissect.target.plugins.os.unix._os import UnixPlugin @@ -76,3 +77,29 @@ def test_gnome_trash(target_unix_users: Target, fs_unix: VirtualFilesystem) -> N 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 + + +@patch( + "dissect.target.filesystem.LayerFilesystem.mounts", + property(MagicMock(return_value={"/tmp/example": None, "/mnt/example": None})), +) +def test_gnome_trash_mounts(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + """test if GNOME Trash plugin finds Trash files in mounted devices from ``/etc/fstab``, ``/mnt`` and ``/media``.""" + + 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")) + fs_unix.map_dir("mnt/example/.Trash-1234", absolute_path("_data/plugins/os/unix/trash")) + fs_unix.map_dir("tmp/example/.Trash-5678", absolute_path("_data/plugins/os/unix/trash")) + fs_unix.map_dir("media/user/example/.Trash-1000", absolute_path("_data/plugins/os/unix/trash")) + + target_unix_users.add_plugin(UnixPlugin) + plugin = GnomeTrashPlugin(target_unix_users) + + assert sorted([str(t) for _, t in plugin.trashes]) == [ + "/home/user/.local/share/Trash", + "/media/user/example/.Trash-1000", + "/mnt/example/.Trash-1234", + "/tmp/example/.Trash-5678", + ] + + assert len(list(plugin.trash())) == 11 * len(plugin.trashes) From d0fa33bf9a7428ff20755d819adcfc8af22e36de Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:48:57 +0200 Subject: [PATCH 2/2] update rst docstring --- dissect/target/plugins/os/unix/trash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/unix/trash.py b/dissect/target/plugins/os/unix/trash.py index e1177ccf7..7125f9b3d 100644 --- a/dissect/target/plugins/os/unix/trash.py +++ b/dissect/target/plugins/os/unix/trash.py @@ -62,7 +62,8 @@ def trash(self) -> Iterator[TrashRecord]: 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/*``. + Also parses media trash locations such as ``/media/$USER/$Label/.Trash-*``, ``/mnt/$Label/.Trash-*`` and other + locations as defined in ``/etc/fstab``. Resources: - https://specifications.freedesktop.org/trash-spec/latest/