From 38c01453da3b61d22992ae8fee4b82bc8401e35a Mon Sep 17 00:00:00 2001 From: Stefan de Reuver <9864602+Horofic@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:17:30 +0200 Subject: [PATCH] Improve Linux OS detection (#809) Co-authored-by: Schamper <1254028+Schamper@users.noreply.github.com> --- .../target/plugins/os/unix/bsd/freebsd/_os.py | 8 ++- dissect/target/plugins/os/unix/linux/_os.py | 24 +++++---- tests/conftest.py | 12 ++++- tests/plugins/os/unix/linux/test__os.py | 49 +++++++++++++++++++ tests/plugins/os/windows/test__os.py | 32 +++++++++--- 5 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 tests/plugins/os/unix/linux/test__os.py diff --git a/dissect/target/plugins/os/unix/bsd/freebsd/_os.py b/dissect/target/plugins/os/unix/bsd/freebsd/_os.py index b233a50c2..667326334 100644 --- a/dissect/target/plugins/os/unix/bsd/freebsd/_os.py +++ b/dissect/target/plugins/os/unix/bsd/freebsd/_os.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from dissect.target.filesystem import Filesystem from dissect.target.plugin import export from dissect.target.plugins.os.unix.bsd._os import BsdPlugin @@ -14,13 +12,13 @@ def __init__(self, target: Target): self._os_release = self._parse_os_release("/bin/freebsd-version*") @classmethod - def detect(cls, target: Target) -> Optional[Filesystem]: + def detect(cls, target: Target) -> Filesystem | None: for fs in target.filesystems: - if fs.exists("/net") or fs.exists("/.sujournal"): + if fs.exists("/net") and (fs.exists("/.sujournal") or fs.exists("/entropy")): return fs return None @export(property=True) - def version(self) -> Optional[str]: + def version(self) -> str | None: return self._os_release.get("USERLAND_VERSION") diff --git a/dissect/target/plugins/os/unix/linux/_os.py b/dissect/target/plugins/os/unix/linux/_os.py index 0b22cc309..6c01cde8b 100644 --- a/dissect/target/plugins/os/unix/linux/_os.py +++ b/dissect/target/plugins/os/unix/linux/_os.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional from dissect.target.filesystem import Filesystem from dissect.target.helpers.network_managers import ( @@ -8,6 +9,8 @@ ) from dissect.target.plugin import OperatingSystem, export from dissect.target.plugins.os.unix._os import UnixPlugin +from dissect.target.plugins.os.unix.bsd.osx._os import MacPlugin +from dissect.target.plugins.os.windows._os import WindowsPlugin from dissect.target.target import Target log = logging.getLogger(__name__) @@ -20,17 +23,13 @@ def __init__(self, target: Target): self.network_manager.discover() @classmethod - def detect(cls, target: Target) -> Optional[Filesystem]: + def detect(cls, target: Target) -> Filesystem | None: for fs in target.filesystems: if ( - fs.exists("/var") - and fs.exists("/etc") - and fs.exists("/opt") - or (fs.exists("/sys") or fs.exists("/proc")) - and not fs.exists("/Library") - ): + (fs.exists("/var") and fs.exists("/etc") and fs.exists("/opt")) + or (fs.exists("/sys/module") or fs.exists("/proc/sys")) + ) and not (MacPlugin.detect(target) or WindowsPlugin.detect(target)): return fs - return None @export(property=True) def ips(self) -> list[str]: @@ -68,7 +67,7 @@ def netmask(self) -> list[str]: return self.network_manager.get_config_value("netmask") @export(property=True) - def version(self) -> str: + def version(self) -> str | None: distrib_description = self._os_release.get("DISTRIB_DESCRIPTION", "") name = self._os_release.get("NAME", "") or self._os_release.get("DISTRIB_ID", "") version = ( @@ -78,10 +77,9 @@ def version(self) -> str: ) if len(f"{name} {version}") > len(distrib_description): - return f"{name} {version}" + distrib_description = f"{name} {version}" - else: - return distrib_description + return distrib_description or None @export(property=True) def os(self) -> str: diff --git a/tests/conftest.py b/tests/conftest.py index c31f29479..75d027714 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import pathlib import tempfile import textwrap from io import BytesIO -from typing import Callable, Iterator, Optional +from typing import Callable, Iterator import pytest @@ -50,7 +52,7 @@ def make_mock_target(tmp_path: pathlib.Path) -> Iterator[Target]: def make_os_target( tmp_path: pathlib.Path, os_plugin: type[OSPlugin], - root_fs: Optional[Filesystem] = None, + root_fs: Filesystem | None = None, apply_target: bool = True, ) -> Target: mock_target = next(make_mock_target(tmp_path)) @@ -126,6 +128,12 @@ def fs_suse() -> Iterator[VirtualFilesystem]: yield fs +@pytest.fixture +def fs_linux_sys(fs_linux: VirtualFilesystem) -> Iterator[VirtualFilesystem]: + fs_linux.makedirs("sys") + yield fs_linux + + @pytest.fixture def fs_linux_proc(fs_linux: VirtualFilesystem) -> Iterator[VirtualFilesystem]: fs = fs_linux diff --git a/tests/plugins/os/unix/linux/test__os.py b/tests/plugins/os/unix/linux/test__os.py new file mode 100644 index 000000000..b379b2ded --- /dev/null +++ b/tests/plugins/os/unix/linux/test__os.py @@ -0,0 +1,49 @@ +import pathlib +from typing import Iterator + +import pytest + +from dissect.target import Target +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.os.unix.linux._os import LinuxPlugin +from tests.conftest import make_os_target + + +@pytest.fixture +def target_linux_proc_sys(tmp_path: pathlib.Path) -> Iterator[Target]: + root_fs = VirtualFilesystem() + root_fs.makedirs("/proc/sys") + root_fs.makedirs("/sys/module") + target_linux_proc_sys = make_os_target(tmp_path, LinuxPlugin, root_fs) + + yield target_linux_proc_sys + + +@pytest.fixture +def target_linux_windows_folder(tmp_path: pathlib.Path) -> Iterator[Target]: + root_fs = VirtualFilesystem() + root_fs.makedirs("/windows") + root_fs.makedirs("/var") + root_fs.makedirs("/etc") + root_fs.makedirs("/opt") + + target_linux_proc_sys = make_os_target(tmp_path, LinuxPlugin, root_fs) + + yield target_linux_proc_sys + + +def test_linux_os(target_linux: Target) -> None: + target_linux.add_plugin(LinuxPlugin) + + assert target_linux.os == "linux" + + +def test_linux_os_windows_folder(target_linux_windows_folder: Target) -> None: + target_linux_windows_folder.add_plugin(LinuxPlugin) + assert target_linux_windows_folder._os_plugin.detect(target_linux_windows_folder) is not None + assert target_linux_windows_folder.os == "linux" + + +def test_linux_os_proc_sys(target_linux_proc_sys: Target) -> None: + assert target_linux_proc_sys._os_plugin.detect(target_linux_proc_sys) is not None + assert target_linux_proc_sys.os == "linux" diff --git a/tests/plugins/os/windows/test__os.py b/tests/plugins/os/windows/test__os.py index 8cc9b6422..ae6540152 100644 --- a/tests/plugins/os/windows/test__os.py +++ b/tests/plugins/os/windows/test__os.py @@ -1,8 +1,12 @@ -from typing import Any, Optional +from __future__ import annotations + +from typing import Any, Iterator import pytest +from dissect.target.filesystem import Filesystem from dissect.target.helpers.regutil import VirtualKey, VirtualValue +from dissect.target.plugins.os.unix.linux._os import LinuxPlugin from dissect.target.plugins.os.windows._os import WindowsPlugin from dissect.target.plugins.os.windows.registry import RegistryPlugin from dissect.target.target import Target @@ -31,7 +35,13 @@ def win_plugin(version_target: Target): return WindowsPlugin(version_target) -def map_version_value(target: Target, name: Optional[str], value: Any): +@pytest.fixture +def target_win_linux_folders(target_win: Filesystem, fs_linux_sys: Filesystem) -> Iterator[Target]: + target_win.fs.mount("/", fs_linux_sys) + yield target_win + + +def map_version_value(target: Target, name: str | None, value: Any): if name is not None: hive = target.registry._root hive.map_value(CURRENT_VERSION_KEY, name, VirtualValue(hive, name, value)) @@ -54,7 +64,7 @@ def assert_value(result: Any, value: Any): def test_windowsplugin__legacy_curre_ntversion( version_target: Target, win_plugin: WindowsPlugin, - name: Optional[str], + name: str | None, value: Any, ): map_version_value(version_target, name, value) @@ -73,7 +83,7 @@ def test_windowsplugin__legacy_curre_ntversion( def test_windowsplugin__major_version( version_target: Target, win_plugin: WindowsPlugin, - name: Optional[str], + name: str | None, value: Any, ): map_version_value(version_target, name, value) @@ -92,7 +102,7 @@ def test_windowsplugin__major_version( def test_windowsplugin__minor_version( version_target: Target, win_plugin: WindowsPlugin, - name: Optional[str], + name: str | None, value: Any, ): map_version_value(version_target, name, value) @@ -129,7 +139,7 @@ def test_windowsplugin__nt_version( version_target: Target, win_plugin: WindowsPlugin, keys: list[tuple[str, Any]], - value: Optional[str], + value: str | None, ): for key_name, key_value in keys: map_version_value(version_target, key_name, key_value) @@ -228,10 +238,18 @@ def test_windowsplugin_version( version_target: Target, win_plugin: WindowsPlugin, keys: list[tuple[str, Any]], - value: Optional[str], + value: str | None, ): for key_name, key_value in keys: map_version_value(version_target, key_name, key_value) result = win_plugin.version assert_value(result, value) + + +def test_windows_os_detection_with_linux_folders(target_win_linux_folders: Target) -> None: + fs_linux = LinuxPlugin.detect(target_win_linux_folders) + fs_windows = WindowsPlugin.detect(target_win_linux_folders) + + assert fs_linux is None + assert fs_windows is not None