From 224cede2efbdd5bae6f7887768ce339a6b2c504d Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:05:26 +0200 Subject: [PATCH 01/11] added a plugin to parser /proc//maps --- dissect/target/plugins/os/unix/_os.py | 65 +++++++++++++++--- dissect/target/plugins/os/unix/linux/mount.py | 67 +++++++++++++++++++ dissect/target/plugins/os/unix/linux/proc.py | 34 ++++++++-- 3 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 dissect/target/plugins/os/unix/linux/mount.py diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index c12ccb7039..cf0fdbac9b 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import shlex import uuid from typing import TYPE_CHECKING @@ -446,6 +447,53 @@ def _get_architecture(self, os: str = "unix", path: Path | str = "/bin/ls") -> s return f"{arch}_32-{os}" if bits == 1 and arch[-2:] != "32" else f"{arch}-{os}" +def parse_fstab_entry(entry: str, log: logging.Logger = log) -> tuple[str, str, str, str, bool, int] | None: + """Parse a single fstab entry according to the man page fstab(5). + + At the man page, the structure of a fstab entry is: + + """ + entry = entry.strip() + if not entry or entry.startswith("#"): + return None + + # Fields are separated by tabs or spaces. + parts = shlex.split(entry) + + # + if len(parts) < 2: + log.warning("Invalid fstab entry, not enough fields: %s", entry) + return None + + # Pad with defaults + parts.extend([""] * (6 - len(parts))) + fs_spec, mount_point, fs_type, options, dump, pass_num = parts + + if not fs_type: + fs_type = "auto" + + if not options: + options = "defaults" + + if dump == "1": + is_dump = True + elif not dump or dump == "0": + is_dump = False + else: + log.warning("Invalid dump: %s", dump) + return None + + if not pass_num: + pass_num = 0 + elif pass_num.isnumeric(): + pass_num = int(pass_num) + else: + log.warning("Invalid pass num: %s", pass_num) + return None + + return fs_spec, mount_point, fs_type, options, is_dump, pass_num + + def parse_fstab( fstab: TargetPath, log: logging.Logger = log, @@ -465,17 +513,12 @@ def parse_fstab( if not fstab.exists(): return - for entry in fstab.open("rt"): - entry = entry.strip() - if entry.startswith("#"): - continue - - entry_parts = re.split(r"\s+", entry) - - if len(entry_parts) != 6: + for line in fstab.open("rt"): + entry = parse_fstab_entry(line, log) + if not entry: continue - dev, mount_point, fs_type, options, _, _ = entry_parts + dev, mount_point, fs_type, options, _, _ = entry if fs_type in SKIP_FS_TYPES: log.warning("Skipped FS type: %s, %s, %s", fs_type, dev, mount_point) @@ -492,9 +535,9 @@ def parse_fstab( # Emulate that here when combining the vg and lv names volume_name = "-".join(part.replace("-", "--") for part in dev.rsplit("/")[-2:]) elif dev.startswith("UUID="): - dev_id = dev.split("=")[1] + dev_id = dev.split("=")[1].strip('"') elif dev.startswith("LABEL="): - volume_name = dev.split("=")[1] + volume_name = dev.split("=")[1].strip('"') elif fs_type == "nfs": # Put the nfs server address in dev_id and the root path in volume_name dev_id, sep, volume_name = dev.partition(":") diff --git a/dissect/target/plugins/os/unix/linux/mount.py b/dissect/target/plugins/os/unix/linux/mount.py new file mode 100644 index 0000000000..98f1ec486f --- /dev/null +++ b/dissect/target/plugins/os/unix/linux/mount.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + +FstabEntryRecord = TargetRecordDescriptor( + "linux/etc/fstab", + [ + ("string", "device_path"), + ("string", "mount_path"), + ("string", "fs_type"), + ("string[]", "options"), + ("boolean", "is_dump"), + ("varint", "pass_num") + ], +) + +MountRecord = TargetRecordDescriptor( + "linux/proc/mounts", + [ + ("varint", "pid"), + ] + + FstabEntryRecord.target_fields, +) + + +class MountPlugin(Plugin): + """Linux volatile proc environment plugin.""" + + def check_compatible(self) -> None: + if not self.target.has_function("proc"): + raise UnsupportedPluginError("proc filesystem not available") + + @export(record=MountRecord) + def mounts(self) -> Iterator[MountRecord]: + """Return the mount points for all processes. + + Yields MountRecord with the following fields: + + .. code-block:: text + + pid (varint): The process id (pid) of the process. + device_path (string): The device path. + mount_path (string): The mount path. + fs_type (string): The filesystem type. + fs_mntops (string): The mount options. + fs_freq (varint): The dump frequency. + fs_passno (varint): The pass number. + """ + for process in self.target.proc.processes(): + for mount_entry in process.mounts(): + yield MountRecord( + pid = process.pid, + device_path = mount_entry.fs_spec, + mount_path = mount_entry.mount_path, + fs_type = mount_entry.fs_type, + options = mount_entry.options, + is_dump = mount_entry.is_dump, + pass_num = mount_entry.pass_num, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index 89d9a39129..c6239ffba8 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -8,7 +8,7 @@ from ipaddress import IPv4Address, IPv6Address from socket import htonl from struct import pack, unpack -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from dissect.util.ts import from_unix @@ -16,12 +16,12 @@ from dissect.target.filesystem import fsutil from dissect.target.helpers.utils import StrEnum from dissect.target.plugin import Plugin, internal +from dissect.target.plugins.os.unix._os import parse_fstab_entry +from dissect.target.helpers.fsutil import TargetPath if TYPE_CHECKING: from collections.abc import Iterator from datetime import datetime - from pathlib import Path - from typing_extensions import Self from dissect.target.target import Target @@ -154,6 +154,15 @@ class Environ: variable: str contents: str +@dataclass +class FstabEntry: + fs_spec: str + mount_path: str + fs_type: str + options: List[str] + is_dump: bool + pass_num: int + class ProcessStateEnum(StrEnum): R = "Running" # Running @@ -452,7 +461,7 @@ def _parse_proc_stat_entry(self) -> dict[str, str | int]: head = status[:start_name] tail = status[end_name:] - name = status[start_name + 1 : end_name] + name = status[start_name + 1: end_name] status = head + tail for idx, part in enumerate(status.split()[: len(PROC_STAT_NAMES)]): @@ -501,7 +510,7 @@ def _boottime(self) -> int | None: return int(line.split()[1]) return None - def get(self, path: str) -> Path: + def get(self, path: str) -> TargetPath: """Returns a TargetPath relative to this process.""" return self.entry.joinpath(path) @@ -608,6 +617,19 @@ def cmdline(self) -> str: return line + def mounts(self) -> Iterator[FstabEntry]: + """Yields the content of the mount file associated with the process.""" + mount = self.get("mounts") + for line in mount.open("rt"): + entry = parse_fstab_entry(line) + if not entry: + continue + + fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry + options = options.split(",") + + yield FstabEntry(fs_spec, mount_point, fs_type, options, is_dump, pass_num) + def stat(self) -> fsutil.stat_result: """Return a stat entry of the process.""" return self.entry.stat() @@ -646,7 +668,7 @@ def inode_map(self) -> dict[int, list[ProcProcess]]: return map @internal - def iter_proc(self) -> Iterator[Path]: + def iter_proc(self) -> Iterator[TargetPath]: """Yields ``/proc/[pid]`` filesystems entries for every process id (pid) found in procfs.""" yield from self.target.fs.path("/proc").glob("[0-9]*") From 3ef99b307f4bde0030a9772e23f9b43d82351d3a Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+yld2004@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:19:07 +0200 Subject: [PATCH 02/11] added test for mount plugin --- dissect/target/plugins/os/unix/_os.py | 5 ++- dissect/target/plugins/os/unix/linux/mount.py | 31 +++++++++---------- dissect/target/plugins/os/unix/linux/proc.py | 7 +++-- tests/conftest.py | 22 ++++++++++++- tests/plugins/os/unix/linux/test_mounts.py | 30 ++++++++++++++++++ 5 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 tests/plugins/os/unix/linux/test_mounts.py diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index cf0fdbac9b..4f8de00ba6 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re import shlex import uuid from typing import TYPE_CHECKING @@ -464,6 +463,10 @@ def parse_fstab_entry(entry: str, log: logging.Logger = log) -> tuple[str, str, if len(parts) < 2: log.warning("Invalid fstab entry, not enough fields: %s", entry) return None + if len(parts) > 6: + log.warning("Invalid fstab entry, too many fields: %s", entry) + return None + # Pad with defaults parts.extend([""] * (6 - len(parts))) diff --git a/dissect/target/plugins/os/unix/linux/mount.py b/dissect/target/plugins/os/unix/linux/mount.py index 98f1ec486f..1a481d60e8 100644 --- a/dissect/target/plugins/os/unix/linux/mount.py +++ b/dissect/target/plugins/os/unix/linux/mount.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations from typing import TYPE_CHECKING @@ -17,16 +17,13 @@ ("string", "fs_type"), ("string[]", "options"), ("boolean", "is_dump"), - ("varint", "pass_num") + ("varint", "pass_num"), ], ) MountRecord = TargetRecordDescriptor( "linux/proc/mounts", - [ - ("varint", "pid"), - ] - + FstabEntryRecord.target_fields, + [("varint", "pid"), *FstabEntryRecord.target_fields], ) @@ -43,25 +40,25 @@ def mounts(self) -> Iterator[MountRecord]: Yields MountRecord with the following fields: - .. code-block:: text + . code-block:: text pid (varint): The process id (pid) of the process. device_path (string): The device path. mount_path (string): The mount path. fs_type (string): The filesystem type. - fs_mntops (string): The mount options. - fs_freq (varint): The dump frequency. - fs_passno (varint): The pass number. + options (string[]): The mount options. + is_dump (boolean): The dump frequency flag. + pass_num (varint): The pass number. """ for process in self.target.proc.processes(): for mount_entry in process.mounts(): yield MountRecord( - pid = process.pid, - device_path = mount_entry.fs_spec, - mount_path = mount_entry.mount_path, - fs_type = mount_entry.fs_type, - options = mount_entry.options, - is_dump = mount_entry.is_dump, - pass_num = mount_entry.pass_num, + pid=process.pid, + device_path=mount_entry.fs_spec, + mount_path=mount_entry.mount_path, + fs_type=mount_entry.fs_type, + options=mount_entry.options, + is_dump=mount_entry.is_dump, + pass_num=mount_entry.pass_num, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index c6239ffba8..62306da921 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -8,7 +8,7 @@ from ipaddress import IPv4Address, IPv6Address from socket import htonl from struct import pack, unpack -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from dissect.util.ts import from_unix @@ -17,13 +17,14 @@ from dissect.target.helpers.utils import StrEnum from dissect.target.plugin import Plugin, internal from dissect.target.plugins.os.unix._os import parse_fstab_entry -from dissect.target.helpers.fsutil import TargetPath if TYPE_CHECKING: from collections.abc import Iterator from datetime import datetime + from typing_extensions import Self + from dissect.target.helpers.fsutil import TargetPath from dissect.target.target import Target @@ -159,7 +160,7 @@ class FstabEntry: fs_spec: str mount_path: str fs_type: str - options: List[str] + options: list[str] is_dump: bool pass_num: int diff --git a/tests/conftest.py b/tests/conftest.py index faf53376bd..e43ea14f0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -219,24 +219,41 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: VirtualSymlink(fs, "/proc/1/fd/4", "socket:[1337]"), "test\x00cmdline\x00", "VAR=1", + """ + UUID=1349-vbay-as78-efeh /home ext4 defaults 0 2 + /dev/sdc1 /mnt/windows ntfs-3g ro,uid=1000 0 0 + """ ), ( "proc/2", VirtualSymlink(fs, "/proc/2/fd/4", "socket:[1338]"), "\x00", "VAR=1\x00", + """ + 192.168.1.50:/exports/share /mnt/nfs nfs _netdev,auto 0 0 + UUID=1234-abcd /data ext4 defaults + /dev/sdc2 /mnt/usb ext3 sw 2 0 + """ ), ( "proc/3", VirtualSymlink(fs, "/proc/3/fd/4", "socket:[1339]"), "sshd", "VAR=1", + """ + /dev/sdb2 none swap sw 0 0 + /dev/sdc2 /mnt/usb ext3 sw 1 0 + /dev/sdd1 / ext4 sw 0 a + """ ), ( "proc/1337", VirtualSymlink(fs, "/proc/1337/fd/4", "socket:[1337]"), "acquire\x00-p\x00full\x00--proc\x00", "", + """ + /dev/sdb1 /mnt/my backup ext4 defaults 0 0 + """ ), ) stat_files_data = ( @@ -247,7 +264,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: ) for idx, proc in enumerate(procs): - dir, fd, cmdline, environ = proc + dir, fd, cmdline, environ, mounts = proc fs.makedirs(dir) fs.map_file_entry(fd.path, fd) @@ -255,6 +272,9 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: fs.map_file_fh(dir + "/cmdline", BytesIO(cmdline.encode())) if environ: fs.map_file_fh(dir + "/environ", BytesIO(environ.encode())) + if mounts: + fs.map_file_fh(dir + "/mounts", BytesIO(mounts.encode())) + # symlink acquire process to self fs.link("/proc/1337", "/proc/self") diff --git a/tests/plugins/os/unix/linux/test_mounts.py b/tests/plugins/os/unix/linux/test_mounts.py new file mode 100644 index 0000000000..4a9ba79a1e --- /dev/null +++ b/tests/plugins/os/unix/linux/test_mounts.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from collections import defaultdict +from typing import TYPE_CHECKING + +from dissect.target.plugins.os.unix.linux.proc import ProcPlugin + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +def test_mounts(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) -> None: + target_linux_users.add_plugin(ProcPlugin) + results = list(target_linux_users.mounts()) + + assert len(results) == 6 + + sum_pid_results = defaultdict(int) + for result in results: + sum_pid_results[result.pid] += 1 + + assert sum_pid_results[1] == 2 + + assert sum_pid_results[2] == 2 + + assert sum_pid_results[3] == 2 + + assert sum_pid_results[4] == 0 + From 8c40c10b29e4ed92120153bfedb33f3f4906808a Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:39:46 +0200 Subject: [PATCH 03/11] added plugin that parsers /etc/fstab --- dissect/target/plugins/os/unix/etc/fstab.py | 62 +++++++++++++++++++ dissect/target/plugins/os/unix/linux/mount.py | 13 +--- 2 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 dissect/target/plugins/os/unix/etc/fstab.py diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py new file mode 100644 index 0000000000..12e56bc8c7 --- /dev/null +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix._os import parse_fstab_entry + +if TYPE_CHECKING: + from collections.abc import Iterator + +FstabEntryRecord = TargetRecordDescriptor( + "linux/etc/fstab", + [ + ("string", "device_path"), + ("string", "mount_path"), + ("string", "fs_type"), + ("string[]", "options"), + ("boolean", "is_dump"), + ("varint", "pass_num"), + ], +) + + +class FstabPlugin(Plugin): + """Linux fstab file plugin.""" + + def check_compatible(self) -> None: + if not self.target.fs.exists("/etc/fstab"): + raise UnsupportedPluginError("fstab file isn't available") + + @export(record=FstabEntryRecord) + def entries(self) -> Iterator[FstabEntryRecord]: + """Return the mount entries from /etc/fstab. + + Yields FstabEntryRecord with the following fields: + + . code-block:: text + + device_path (string): The device path. + mount_path (string): The mount path. + fs_type (string): The filesystem type. + options (string[]): The mount options. + is_dump (boolean): The dump frequency flag. + pass_num (varint): The pass number. + """ + fstab_path = self.target.fs.path("/etc/fstab") + + for line in fstab_path.open("rt"): + entry = parse_fstab_entry(line, self.target.log) + if entry: + fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry + yield FstabEntryRecord( + device_path=fs_spec, + mount_path=mount_point, + fs_type=fs_type, + options=options.split(","), + is_dump=is_dump, + pass_num=pass_num, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/linux/mount.py b/dissect/target/plugins/os/unix/linux/mount.py index 1a481d60e8..a787476843 100644 --- a/dissect/target/plugins/os/unix/linux/mount.py +++ b/dissect/target/plugins/os/unix/linux/mount.py @@ -5,22 +5,11 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.etc.fstab import FstabEntryRecord if TYPE_CHECKING: from collections.abc import Iterator -FstabEntryRecord = TargetRecordDescriptor( - "linux/etc/fstab", - [ - ("string", "device_path"), - ("string", "mount_path"), - ("string", "fs_type"), - ("string[]", "options"), - ("boolean", "is_dump"), - ("varint", "pass_num"), - ], -) - MountRecord = TargetRecordDescriptor( "linux/proc/mounts", [("varint", "pid"), *FstabEntryRecord.target_fields], From e2e5cf98daebf08d89235df013e22d010153efd0 Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:44:08 +0200 Subject: [PATCH 04/11] added test for the fstab plugin --- dissect/target/plugins/os/unix/etc/fstab.py | 29 ++++++----- .../os/unix/linux/{mount.py => mounts.py} | 0 tests/_data/plugins/os/unix/etc/fstab | 3 ++ tests/plugins/os/unix/test__os.py | 49 ++---------------- tests/plugins/os/unix/test_fstab.py | 51 +++++++++++++++++++ 5 files changed, 76 insertions(+), 56 deletions(-) rename dissect/target/plugins/os/unix/linux/{mount.py => mounts.py} (100%) create mode 100644 tests/_data/plugins/os/unix/etc/fstab create mode 100644 tests/plugins/os/unix/test_fstab.py diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py index 12e56bc8c7..d5e8b6db9d 100644 --- a/dissect/target/plugins/os/unix/etc/fstab.py +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -26,12 +26,14 @@ class FstabPlugin(Plugin): """Linux fstab file plugin.""" + __namespace__ = "etc" + def check_compatible(self) -> None: if not self.target.fs.exists("/etc/fstab"): raise UnsupportedPluginError("fstab file isn't available") @export(record=FstabEntryRecord) - def entries(self) -> Iterator[FstabEntryRecord]: + def fstab(self) -> Iterator[FstabEntryRecord]: """Return the mount entries from /etc/fstab. Yields FstabEntryRecord with the following fields: @@ -49,14 +51,17 @@ def entries(self) -> Iterator[FstabEntryRecord]: for line in fstab_path.open("rt"): entry = parse_fstab_entry(line, self.target.log) - if entry: - fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry - yield FstabEntryRecord( - device_path=fs_spec, - mount_path=mount_point, - fs_type=fs_type, - options=options.split(","), - is_dump=is_dump, - pass_num=pass_num, - _target=self.target, - ) + + if not entry: + continue + + fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry + yield FstabEntryRecord( + device_path=fs_spec, + mount_path=mount_point, + fs_type=fs_type, + options=options.split(","), + is_dump=is_dump, + pass_num=pass_num, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/linux/mount.py b/dissect/target/plugins/os/unix/linux/mounts.py similarity index 100% rename from dissect/target/plugins/os/unix/linux/mount.py rename to dissect/target/plugins/os/unix/linux/mounts.py diff --git a/tests/_data/plugins/os/unix/etc/fstab b/tests/_data/plugins/os/unix/etc/fstab new file mode 100644 index 0000000000..afc1382f8a --- /dev/null +++ b/tests/_data/plugins/os/unix/etc/fstab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebac5687fc1e1d718f4dcd94d9498dded7aa9a2835d67898b7ebb945c505fda0 +size 1601 diff --git a/tests/plugins/os/unix/test__os.py b/tests/plugins/os/unix/test__os.py index 18e970683d..c9261323e7 100644 --- a/tests/plugins/os/unix/test__os.py +++ b/tests/plugins/os/unix/test__os.py @@ -1,6 +1,5 @@ from __future__ import annotations -import tempfile from io import BytesIO from typing import TYPE_CHECKING from unittest.mock import Mock, patch @@ -12,56 +11,18 @@ from dissect.target.filesystem import VirtualFilesystem from dissect.target.plugins.os.unix._os import UnixPlugin, parse_fstab from dissect.target.target import Target +from tests._utils import absolute_path if TYPE_CHECKING: from pathlib import Path -FSTAB_CONTENT = """ -# /etc/fstab: static file system information. -# -# Use 'blkid' to print the universally unique identifier for a -# device; this may be used with UUID= as a more robust way to name devices -# that works even if disks are added and removed. See fstab(5). -# -# - -proc /proc proc nodev,noexec,nosuid 0 0 - -UUID=563f929e-ab4b-4741-b0f4-e3843c9a7a6a / ext4 defaults,discard 0 0 - -UUID=5d1f1508-069b-4274-9bfa-ae2bf7ffb5e0 /home ext4 defaults 0 2 - -UUID=be0afdc6-10bb-4744-a71c-02e0e2812160 none swap sw 0 0 - -/dev/mapper/vgubuntu-swap_1 none swap sw 0 0 - -UUID=28a25297-9825-4f87-ac41-f9c20cd5db4f /boot ext4 defaults 0 2 - -UUID=F631-BECA /boot/efi vfat defaults,discard,umask=0077 0 0 - -/dev/disk/cloud/azure_resource-part1 /mnt auto defaults,nofail,x-systemd.requires=cloud-init.service,comment=cloudconfig 0 2 - -/dev/mapper/vg--main-lv--var /var auto default 0 2 - -/dev/vg-main/lv-data /data auto default 0 2 - -/dev/disk/by-uuid/af0b9707-0945-499a-a37d-4da23d8dd245 /moredata auto default 0 2 - -LABEL=foo /foo auto default 0 2 - -localhost:/home/user/nfstest /mnt/nfs nfs ro 0 0 -""" # noqa - def test_parse_fstab(tmp_path: Path) -> None: - with tempfile.NamedTemporaryFile(dir=tmp_path, delete=False) as tf: - tf.write(FSTAB_CONTENT.encode("ascii")) - tf.close() - - fs = VirtualFilesystem() - fs.map_file("/etc/fstab", tf.name) + fs = VirtualFilesystem() + fstab_file = absolute_path("_data/plugins/os/unix/etc/fstab") + fs.map_file("/etc/fstab", fstab_file) - records = list(parse_fstab(fs.path("/etc/fstab"))) + records = list(parse_fstab(fs.path("/etc/fstab"))) # 11 input records minus # 2 unsupported mount devices (proc, /dev/disk/cloud/azure_resource-part1) diff --git a/tests/plugins/os/unix/test_fstab.py b/tests/plugins/os/unix/test_fstab.py new file mode 100644 index 0000000000..3cc91c9e60 --- /dev/null +++ b/tests/plugins/os/unix/test_fstab.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from io import BytesIO +from textwrap import dedent +from typing import TYPE_CHECKING + +from dissect.target.plugins.os.unix.etc.fstab import FstabPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + import pytest + + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +def test_etc_fstab_plugin(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + fstab_file = absolute_path("_data/plugins/os/unix/etc/fstab") + fs_unix.map_file("/etc/fstab", fstab_file) + target_unix_users.add_plugin(FstabPlugin) + + results = list(target_unix_users.etc.fstab()) + assert len(results) == 13 + + + +def test_etc_fstab_plugin_invalid( + caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem +) -> None: + """Test if we can parse invalid fstab entries.""" + fstab_invalid = """ + UUID=1349-vbay-as78-efeh /home ext4 defaults 0 2 + /dev/sdc1 /mnt/windows ntfs-3g ro,uid=1000 0 0 + 192.168.1.50:/exports/share /mnt/nfs nfs _netdev,auto 0 0 + UUID=1234-abcd /data ext4 defaults + /dev/sdc2 /mnt/usb ext3 sw 2 0 + /dev/sdb2 none swap sw 0 0 + /dev/sdd1 / ext4 sw 0 a + /dev/sdb1 /mnt/my backup ext4 defaults 0 0 + """ + fs_unix.map_file_fh("/etc/fstab", BytesIO(dedent(fstab_invalid).encode())) + target_unix_users.add_plugin(FstabPlugin) + + results = list(target_unix_users.etc.fstab()) + assert len(results) == 5 + assert results[0].device_path == "UUID=1349-vbay-as78-efeh" + assert results[0].mount_path == "/home" + assert results[1].device_path == "/dev/sdc1" + assert results[2].fs_type == "nfs" + assert results[3].device_path == "UUID=1234-abcd" + assert results[4].mount_path == "none" From 98394fe7ef08b815d90e958a8c857bf83e80db38 Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:59:41 +0200 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/unix/etc/fstab.py | 4 ++-- dissect/target/plugins/os/unix/linux/mounts.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py index d5e8b6db9d..40ac495c7c 100644 --- a/dissect/target/plugins/os/unix/etc/fstab.py +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -30,11 +30,11 @@ class FstabPlugin(Plugin): def check_compatible(self) -> None: if not self.target.fs.exists("/etc/fstab"): - raise UnsupportedPluginError("fstab file isn't available") + raise UnsupportedPluginError("No fstab file found on target") @export(record=FstabEntryRecord) def fstab(self) -> Iterator[FstabEntryRecord]: - """Return the mount entries from /etc/fstab. + """Return the mount entries from ``/etc/fstab``. Yields FstabEntryRecord with the following fields: diff --git a/dissect/target/plugins/os/unix/linux/mounts.py b/dissect/target/plugins/os/unix/linux/mounts.py index a787476843..064e6ff37f 100644 --- a/dissect/target/plugins/os/unix/linux/mounts.py +++ b/dissect/target/plugins/os/unix/linux/mounts.py @@ -12,7 +12,7 @@ MountRecord = TargetRecordDescriptor( "linux/proc/mounts", - [("varint", "pid"), *FstabEntryRecord.target_fields], + [("varint", "pid"), *FstabEntryRecord.target_fields,], ) From 1cb43ba8d6dad18bb8edc076a93a376b18cc6e06 Mon Sep 17 00:00:00 2001 From: Yuval Date: Mon, 16 Mar 2026 14:10:26 +0200 Subject: [PATCH 06/11] CR fix --- dissect/target/plugins/os/unix/_os.py | 23 +++++++------ dissect/target/plugins/os/unix/etc/fstab.py | 33 ++++++++++--------- .../target/plugins/os/unix/linux/mounts.py | 9 +++-- dissect/target/plugins/os/unix/linux/proc.py | 22 ++++++++----- 4 files changed, 47 insertions(+), 40 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 4f8de00ba6..4a1413ba7e 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -446,7 +446,7 @@ def _get_architecture(self, os: str = "unix", path: Path | str = "/bin/ls") -> s return f"{arch}_32-{os}" if bits == 1 and arch[-2:] != "32" else f"{arch}-{os}" -def parse_fstab_entry(entry: str, log: logging.Logger = log) -> tuple[str, str, str, str, bool, int] | None: +def parse_fstab_entry(entry: str) -> tuple[str, str, str, str, bool, int]: """Parse a single fstab entry according to the man page fstab(5). At the man page, the structure of a fstab entry is: @@ -454,18 +454,17 @@ def parse_fstab_entry(entry: str, log: logging.Logger = log) -> tuple[str, str, """ entry = entry.strip() if not entry or entry.startswith("#"): - return None + raise ValueError("Empty or commented line") # Fields are separated by tabs or spaces. parts = shlex.split(entry) # if len(parts) < 2: - log.warning("Invalid fstab entry, not enough fields: %s", entry) - return None + raise ValueError(f"Invalid fstab entry, not enough fields: {entry}") + if len(parts) > 6: - log.warning("Invalid fstab entry, too many fields: %s", entry) - return None + raise ValueError(f"Invalid fstab entry, too many fields: {entry}") # Pad with defaults @@ -483,16 +482,14 @@ def parse_fstab_entry(entry: str, log: logging.Logger = log) -> tuple[str, str, elif not dump or dump == "0": is_dump = False else: - log.warning("Invalid dump: %s", dump) - return None + raise ValueError(f"Invalid dump: {dump}") if not pass_num: pass_num = 0 elif pass_num.isnumeric(): pass_num = int(pass_num) else: - log.warning("Invalid pass num: %s", pass_num) - return None + raise ValueError(f"Invalid pass num: {pass_num}") return fs_spec, mount_point, fs_type, options, is_dump, pass_num @@ -517,8 +514,10 @@ def parse_fstab( return for line in fstab.open("rt"): - entry = parse_fstab_entry(line, log) - if not entry: + try: + entry = parse_fstab_entry(line) + except ValueError as e: + log.warning("Failed to parse fstab entry: %s", e) continue dev, mount_point, fs_type, options, _, _ = entry diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py index 40ac495c7c..35a2f3e087 100644 --- a/dissect/target/plugins/os/unix/etc/fstab.py +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -48,20 +48,21 @@ def fstab(self) -> Iterator[FstabEntryRecord]: pass_num (varint): The pass number. """ fstab_path = self.target.fs.path("/etc/fstab") + with fstab_path.open("rt") as fstab_file: + for line in fstab_file: + try: + entry = parse_fstab_entry(line) + except ValueError as e: + self.target.log.warning("Failed to parse fstab entry: %s", e) + continue - for line in fstab_path.open("rt"): - entry = parse_fstab_entry(line, self.target.log) - - if not entry: - continue - - fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry - yield FstabEntryRecord( - device_path=fs_spec, - mount_path=mount_point, - fs_type=fs_type, - options=options.split(","), - is_dump=is_dump, - pass_num=pass_num, - _target=self.target, - ) + fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry + yield FstabEntryRecord( + device_path=fs_spec, + mount_path=mount_point, + fs_type=fs_type, + options=options.split(","), + is_dump=is_dump, + pass_num=pass_num, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/linux/mounts.py b/dissect/target/plugins/os/unix/linux/mounts.py index 064e6ff37f..41538987c8 100644 --- a/dissect/target/plugins/os/unix/linux/mounts.py +++ b/dissect/target/plugins/os/unix/linux/mounts.py @@ -12,12 +12,15 @@ MountRecord = TargetRecordDescriptor( "linux/proc/mounts", - [("varint", "pid"), *FstabEntryRecord.target_fields,], + [ + ("varint", "pid"), + *FstabEntryRecord.target_fields, + ], ) -class MountPlugin(Plugin): - """Linux volatile proc environment plugin.""" +class MountsPlugin(Plugin): + """Linux volatile proc mounts plugin.""" def check_compatible(self) -> None: if not self.target.has_function("proc"): diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index 62306da921..4c03964833 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -155,6 +155,7 @@ class Environ: variable: str contents: str + @dataclass class FstabEntry: fs_spec: str @@ -462,7 +463,7 @@ def _parse_proc_stat_entry(self) -> dict[str, str | int]: head = status[:start_name] tail = status[end_name:] - name = status[start_name + 1: end_name] + name = status[start_name + 1 : end_name] status = head + tail for idx, part in enumerate(status.split()[: len(PROC_STAT_NAMES)]): @@ -620,16 +621,19 @@ def cmdline(self) -> str: def mounts(self) -> Iterator[FstabEntry]: """Yields the content of the mount file associated with the process.""" - mount = self.get("mounts") - for line in mount.open("rt"): - entry = parse_fstab_entry(line) - if not entry: - continue + mount_path = self.get("mounts") + with mount_path.open("rt") as mount_file: + for line in mount_file: + try: + entry = parse_fstab_entry(line) + except ValueError as e: + self.target.log.warning("Failed to parse fstab entry: %s", e) + continue - fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry - options = options.split(",") + fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry + options = options.split(",") - yield FstabEntry(fs_spec, mount_point, fs_type, options, is_dump, pass_num) + yield FstabEntry(fs_spec, mount_point, fs_type, options, is_dump, pass_num) def stat(self) -> fsutil.stat_result: """Return a stat entry of the process.""" From aadae5f908ca5ed052d36f96dc979639556ac0f6 Mon Sep 17 00:00:00 2001 From: Yuval Date: Mon, 16 Mar 2026 15:06:13 +0200 Subject: [PATCH 07/11] ruff fix --- dissect/target/plugins/os/unix/_os.py | 1 - dissect/target/plugins/os/unix/linux/mounts.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 4a1413ba7e..b4bcc8a52d 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -466,7 +466,6 @@ def parse_fstab_entry(entry: str) -> tuple[str, str, str, str, bool, int]: if len(parts) > 6: raise ValueError(f"Invalid fstab entry, too many fields: {entry}") - # Pad with defaults parts.extend([""] * (6 - len(parts))) fs_spec, mount_point, fs_type, options, dump, pass_num = parts diff --git a/dissect/target/plugins/os/unix/linux/mounts.py b/dissect/target/plugins/os/unix/linux/mounts.py index 41538987c8..2d12a3a58b 100644 --- a/dissect/target/plugins/os/unix/linux/mounts.py +++ b/dissect/target/plugins/os/unix/linux/mounts.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations from typing import TYPE_CHECKING From 4a25b21ef94f51b1c2b3f2f4677fa60040ee95fc Mon Sep 17 00:00:00 2001 From: Yuval Date: Mon, 16 Mar 2026 15:48:29 +0200 Subject: [PATCH 08/11] more fixes --- dissect/target/plugins/os/unix/etc/fstab.py | 2 +- dissect/target/plugins/os/unix/linux/mounts.py | 2 +- dissect/target/plugins/os/unix/linux/proc.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py index 35a2f3e087..4801149e2a 100644 --- a/dissect/target/plugins/os/unix/etc/fstab.py +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -36,7 +36,7 @@ def check_compatible(self) -> None: def fstab(self) -> Iterator[FstabEntryRecord]: """Return the mount entries from ``/etc/fstab``. - Yields FstabEntryRecord with the following fields: + Yields ``FstabEntryRecord`` with the following fields: . code-block:: text diff --git a/dissect/target/plugins/os/unix/linux/mounts.py b/dissect/target/plugins/os/unix/linux/mounts.py index 2d12a3a58b..e734a383ee 100644 --- a/dissect/target/plugins/os/unix/linux/mounts.py +++ b/dissect/target/plugins/os/unix/linux/mounts.py @@ -30,7 +30,7 @@ def check_compatible(self) -> None: def mounts(self) -> Iterator[MountRecord]: """Return the mount points for all processes. - Yields MountRecord with the following fields: + Yields ``MountRecord`` with the following fields: . code-block:: text diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index 4c03964833..3c0bad24f8 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -622,6 +622,10 @@ def cmdline(self) -> str: def mounts(self) -> Iterator[FstabEntry]: """Yields the content of the mount file associated with the process.""" mount_path = self.get("mounts") + + if not (mount_path.exists() and mount_path.is_file()): + return + with mount_path.open("rt") as mount_file: for line in mount_file: try: From 0374b3ab9c354153cfc8dd5ba64d28b98d1ea9fb Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:40:46 +0200 Subject: [PATCH 09/11] more ruff fixes --- tests/conftest.py | 9 ++++----- tests/plugins/os/unix/linux/test_mounts.py | 1 - tests/plugins/os/unix/test_fstab.py | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e43ea14f0b..54eff2100d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -222,7 +222,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: """ UUID=1349-vbay-as78-efeh /home ext4 defaults 0 2 /dev/sdc1 /mnt/windows ntfs-3g ro,uid=1000 0 0 - """ + """, ), ( "proc/2", @@ -233,7 +233,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: 192.168.1.50:/exports/share /mnt/nfs nfs _netdev,auto 0 0 UUID=1234-abcd /data ext4 defaults /dev/sdc2 /mnt/usb ext3 sw 2 0 - """ + """, ), ( "proc/3", @@ -244,7 +244,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: /dev/sdb2 none swap sw 0 0 /dev/sdc2 /mnt/usb ext3 sw 1 0 /dev/sdd1 / ext4 sw 0 a - """ + """, ), ( "proc/1337", @@ -253,7 +253,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: "", """ /dev/sdb1 /mnt/my backup ext4 defaults 0 0 - """ + """, ), ) stat_files_data = ( @@ -275,7 +275,6 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: if mounts: fs.map_file_fh(dir + "/mounts", BytesIO(mounts.encode())) - # symlink acquire process to self fs.link("/proc/1337", "/proc/self") diff --git a/tests/plugins/os/unix/linux/test_mounts.py b/tests/plugins/os/unix/linux/test_mounts.py index 4a9ba79a1e..178ef17b09 100644 --- a/tests/plugins/os/unix/linux/test_mounts.py +++ b/tests/plugins/os/unix/linux/test_mounts.py @@ -27,4 +27,3 @@ def test_mounts(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) -> assert sum_pid_results[3] == 2 assert sum_pid_results[4] == 0 - diff --git a/tests/plugins/os/unix/test_fstab.py b/tests/plugins/os/unix/test_fstab.py index 3cc91c9e60..2463843ed0 100644 --- a/tests/plugins/os/unix/test_fstab.py +++ b/tests/plugins/os/unix/test_fstab.py @@ -23,7 +23,6 @@ def test_etc_fstab_plugin(target_unix_users: Target, fs_unix: VirtualFilesystem) assert len(results) == 13 - def test_etc_fstab_plugin_invalid( caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem ) -> None: From 724f7acf3ab4f7c2299bd2d1f0b9d68feca6fbe5 Mon Sep 17 00:00:00 2001 From: Yld2004 <77771298+Yld2004@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:20:59 +0200 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> Co-authored-by: Yld2004 <77771298+Yld2004@users.noreply.github.com> --- dissect/target/plugins/os/unix/_os.py | 5 +++-- dissect/target/plugins/os/unix/etc/fstab.py | 10 +++++----- dissect/target/plugins/os/unix/linux/mounts.py | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index b4bcc8a52d..941c269f6c 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -449,8 +449,9 @@ def _get_architecture(self, os: str = "unix", path: Path | str = "/bin/ls") -> s def parse_fstab_entry(entry: str) -> tuple[str, str, str, str, bool, int]: """Parse a single fstab entry according to the man page fstab(5). - At the man page, the structure of a fstab entry is: - + According to the man page, the structure of a fstab entry is:: + + """ entry = entry.strip() if not entry or entry.startswith("#"): diff --git a/dissect/target/plugins/os/unix/etc/fstab.py b/dissect/target/plugins/os/unix/etc/fstab.py index 4801149e2a..fd27ab105d 100644 --- a/dissect/target/plugins/os/unix/etc/fstab.py +++ b/dissect/target/plugins/os/unix/etc/fstab.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from collections.abc import Iterator -FstabEntryRecord = TargetRecordDescriptor( +FstabRecord = TargetRecordDescriptor( "linux/etc/fstab", [ ("string", "device_path"), @@ -32,11 +32,11 @@ def check_compatible(self) -> None: if not self.target.fs.exists("/etc/fstab"): raise UnsupportedPluginError("No fstab file found on target") - @export(record=FstabEntryRecord) - def fstab(self) -> Iterator[FstabEntryRecord]: + @export(record=FstabRecord) + def fstab(self) -> Iterator[FstabRecord]: """Return the mount entries from ``/etc/fstab``. - Yields ``FstabEntryRecord`` with the following fields: + Yields ``FstabRecord`` with the following fields: . code-block:: text @@ -57,7 +57,7 @@ def fstab(self) -> Iterator[FstabEntryRecord]: continue fs_spec, mount_point, fs_type, options, is_dump, pass_num = entry - yield FstabEntryRecord( + yield FstabRecord( device_path=fs_spec, mount_path=mount_point, fs_type=fs_type, diff --git a/dissect/target/plugins/os/unix/linux/mounts.py b/dissect/target/plugins/os/unix/linux/mounts.py index e734a383ee..334dfd7e49 100644 --- a/dissect/target/plugins/os/unix/linux/mounts.py +++ b/dissect/target/plugins/os/unix/linux/mounts.py @@ -5,7 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.etc.fstab import FstabEntryRecord +from dissect.target.plugins.os.unix.etc.fstab import FstabRecord if TYPE_CHECKING: from collections.abc import Iterator @@ -14,7 +14,7 @@ "linux/proc/mounts", [ ("varint", "pid"), - *FstabEntryRecord.target_fields, + *FstabRecord.target_fields, ], ) From 8815ecab0df2ac3f11ca1e7f3254572fa52945d0 Mon Sep 17 00:00:00 2001 From: Yuval Date: Tue, 17 Mar 2026 22:01:18 +0200 Subject: [PATCH 11/11] more CR fixes --- dissect/target/plugins/os/unix/_os.py | 7 +++++-- tests/plugins/os/unix/etc/__init__.py | 0 tests/plugins/os/unix/{ => etc}/test_fstab.py | 0 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 tests/plugins/os/unix/etc/__init__.py rename tests/plugins/os/unix/{ => etc}/test_fstab.py (100%) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 941c269f6c..f61c7163df 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -450,7 +450,7 @@ def parse_fstab_entry(entry: str) -> tuple[str, str, str, str, bool, int]: """Parse a single fstab entry according to the man page fstab(5). According to the man page, the structure of a fstab entry is:: - + """ entry = entry.strip() @@ -513,7 +513,10 @@ def parse_fstab( if not fstab.exists(): return - for line in fstab.open("rt"): + with fstab.open("rt") as fstab_file: + fstab_data = fstab_file.readlines() + + for line in fstab_data: try: entry = parse_fstab_entry(line) except ValueError as e: diff --git a/tests/plugins/os/unix/etc/__init__.py b/tests/plugins/os/unix/etc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/test_fstab.py b/tests/plugins/os/unix/etc/test_fstab.py similarity index 100% rename from tests/plugins/os/unix/test_fstab.py rename to tests/plugins/os/unix/etc/test_fstab.py