From 1ad42c32cded54317498ce1857166d1ee597e042 Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Tue, 23 Jan 2024 17:40:47 +0100 Subject: [PATCH 1/7] Added fix as per #DIS-2796 --- dissect/target/plugins/os/unix/_os.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 5979ecdd8..00ea69cf0 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -139,21 +139,29 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[ Args: paths (list): list of paths """ - paths = paths or ["/etc/hostname", "/etc/HOSTNAME"] + paths = paths or ["/etc/hostname", "/etc/HOSTNAME", "/etc/sysconfig/network"] hostname_string = None for path in paths: for fs in self.target.filesystems: - if fs.exists(path): + if not fs.exists(path): + continue + if path == "/etc/sysconfig/network": + file_contents = fs.path(path).open("rt").readlines() + for line in file_contents: + if not line.startswith("HOSTNAME"): + continue + hostname_string = line.rstrip().split("=", maxsplit=1)[1] + else: hostname_string = fs.path(path).open("rt").read().rstrip() - if hostname_string and "." in hostname_string: - hostname_string = hostname_string.split(".", maxsplit=1) - hostname_dict = {"hostname": hostname_string[0], "domain": hostname_string[1]} - else: - hostname_dict = {"hostname": hostname_string, "domain": None} + if hostname_string and "." in hostname_string: + hostname_string = hostname_string.split(".", maxsplit=1) + hostname_dict = {"hostname": hostname_string[0], "domain": hostname_string[1]} + else: + hostname_dict = {"hostname": hostname_string, "domain": None} - return hostname_dict + return hostname_dict def _parse_hosts_string(self, paths: Optional[list[str]] = None) -> dict[str, str]: paths = paths or ["/etc/hosts"] From 8613aa6ce2728034a4aef76c4f4f18611a788f76 Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Thu, 25 Jan 2024 16:27:27 +0100 Subject: [PATCH 2/7] Added test for hostname plugin on unix --- tests/plugins/os/unix/test__os.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/plugins/os/unix/test__os.py b/tests/plugins/os/unix/test__os.py index 087681ccd..27b445eff 100644 --- a/tests/plugins/os/unix/test__os.py +++ b/tests/plugins/os/unix/test__os.py @@ -1,8 +1,14 @@ import tempfile +from io import BytesIO +from pathlib import Path +from textwrap import dedent from uuid import UUID +import pytest + from dissect.target.filesystem import VirtualFilesystem from dissect.target.plugins.os.unix._os import parse_fstab +from dissect.target.target import Target FSTAB_CONTENT = """ # /etc/fstab: static file system information. @@ -61,3 +67,44 @@ def test_parse_fstab(tmp_path): (None, "vg--main-lv--var", "/var", "auto", "default"), (None, "vg--main-lv--data", "/data", "auto", "default"), } + + +@pytest.mark.parametrize( + "path, is_domain_joined, expected_hostname, expected_domain", + [ + ("/etc/hostname", True, "myhost", "mydomain.com"), + ("/etc/HOSTNAME", True, "myhost", "mydomain.com"), + ("/etc/sysconfig/network", True, "myhost", "mydomain.com"), + ("/etc/hostname", False, "myhost", None), + ], +) +def test__parse_hostname_string( + target_redhat: Target, + fs_redhat: VirtualFilesystem, + path: Path, + is_domain_joined: bool, + expected_hostname: str, + expected_domain: str, +): + if is_domain_joined: + hostname = "myhost.mydomain.com" + else: + hostname = "myhost" + + if path == "/etc/sysconfig/network": + hostname_file_content = dedent( + f""" + NETWORKING=NO + HOSTNAME={hostname} + GATEWAY=192.168.1.1""" + ) + hostname_file_content = f"HOSTNAME={hostname}" + else: + hostname_file_content = hostname + + fs_redhat.map_file_fh(path, BytesIO(hostname_file_content.encode("ascii"))) + + hostname_dict = target_redhat._os._parse_hostname_string() + + assert hostname_dict["hostname"] == expected_hostname + assert hostname_dict["domain"] == expected_domain From a571d866c34e299e0532f7290b46b953ddf7c584 Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Thu, 25 Jan 2024 16:28:46 +0100 Subject: [PATCH 3/7] Fix bugged functionality for hostname plugin on unix systems --- dissect/target/plugins/os/unix/_os.py | 48 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 00ea69cf0..4b15b321c 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -132,35 +132,43 @@ def domain(self) -> Optional[str]: def os(self) -> str: return OperatingSystem.UNIX.value + def _parse_rh_legacy(self, path): + file_contents = path.open("rt").readlines() + for line in file_contents: + if not line.startswith("HOSTNAME"): + continue + _, _, hostname = line.rstrip().partition("=") + return hostname + def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[dict[str, str]]: """ - Returns a dict with containing the hostname and domain name portion of the path(s) specified + Returns a dict containing the hostname and domain name portion of the path(s) specified Args: paths (list): list of paths """ - paths = paths or ["/etc/hostname", "/etc/HOSTNAME", "/etc/sysconfig/network"] + REDHAT_LEGACY_PATH = self.target.fs.path("/etc/sysconfig/network") + paths = paths or ["/etc/hostname", "/etc/HOSTNAME", REDHAT_LEGACY_PATH] hostname_string = None + hostname_dict = {"hostname": None, "domain": None} for path in paths: - for fs in self.target.filesystems: - if not fs.exists(path): - continue - if path == "/etc/sysconfig/network": - file_contents = fs.path(path).open("rt").readlines() - for line in file_contents: - if not line.startswith("HOSTNAME"): - continue - hostname_string = line.rstrip().split("=", maxsplit=1)[1] - else: - hostname_string = fs.path(path).open("rt").read().rstrip() - - if hostname_string and "." in hostname_string: - hostname_string = hostname_string.split(".", maxsplit=1) - hostname_dict = {"hostname": hostname_string[0], "domain": hostname_string[1]} - else: - hostname_dict = {"hostname": hostname_string, "domain": None} - + path = self.target.fs.path(path) + + if not path.exists(): + continue + + if path == REDHAT_LEGACY_PATH: + hostname_string = self._parse_rh_legacy(path) + else: + hostname_string = path.open("rt").read().rstrip() + + if hostname_string and "." in hostname_string: + hostname_string = hostname_string.split(".", maxsplit=1) + hostname_dict = {"hostname": hostname_string[0], "domain": hostname_string[1]} + else: + hostname_dict = {"hostname": hostname_string, "domain": None} + break return hostname_dict def _parse_hosts_string(self, paths: Optional[list[str]] = None) -> dict[str, str]: From 896686a243f7abc8e8d6dc3e357fbe1a2cc15d2c Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Thu, 25 Jan 2024 17:02:35 +0100 Subject: [PATCH 4/7] Change target type from Redhat to Unix within test__os.py The functionality of the injected target is not RedHat specific, hence a target of type Unix is more fitting. Changed therefore the injected target for the tests to be Unix. (#DIS-2796) --- tests/plugins/os/unix/test__os.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/plugins/os/unix/test__os.py b/tests/plugins/os/unix/test__os.py index 27b445eff..69061a8b8 100644 --- a/tests/plugins/os/unix/test__os.py +++ b/tests/plugins/os/unix/test__os.py @@ -79,8 +79,8 @@ def test_parse_fstab(tmp_path): ], ) def test__parse_hostname_string( - target_redhat: Target, - fs_redhat: VirtualFilesystem, + target_unix: Target, + fs_unix: VirtualFilesystem, path: Path, is_domain_joined: bool, expected_hostname: str, @@ -102,9 +102,9 @@ def test__parse_hostname_string( else: hostname_file_content = hostname - fs_redhat.map_file_fh(path, BytesIO(hostname_file_content.encode("ascii"))) + fs_unix.map_file_fh(path, BytesIO(hostname_file_content.encode("ascii"))) - hostname_dict = target_redhat._os._parse_hostname_string() + hostname_dict = target_unix._os._parse_hostname_string() assert hostname_dict["hostname"] == expected_hostname assert hostname_dict["domain"] == expected_domain From 73546b8355e426414b87203535820a67181f4ae9 Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Thu, 25 Jan 2024 17:06:54 +0100 Subject: [PATCH 5/7] Change the path of redhat's hostname file to be a string rather than type path This commit removes a redundant conversion from str to Path. These can be better compared through getting a string representation from a Path, which is less expensive. --- dissect/target/plugins/os/unix/_os.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 4b15b321c..5d7658dc7 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -147,8 +147,8 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[ Args: paths (list): list of paths """ - REDHAT_LEGACY_PATH = self.target.fs.path("/etc/sysconfig/network") - paths = paths or ["/etc/hostname", "/etc/HOSTNAME", REDHAT_LEGACY_PATH] + redhat_legacy_path = "/etc/sysconfig/network" + paths = paths or ["/etc/hostname", "/etc/HOSTNAME", redhat_legacy_path] hostname_string = None hostname_dict = {"hostname": None, "domain": None} @@ -158,7 +158,7 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[ if not path.exists(): continue - if path == REDHAT_LEGACY_PATH: + if path.as_posix() == redhat_legacy_path: hostname_string = self._parse_rh_legacy(path) else: hostname_string = path.open("rt").read().rstrip() From 097142f671af8dee2ce24840eb100083101e8371 Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Sun, 28 Jan 2024 15:50:12 +0100 Subject: [PATCH 6/7] Add testing to cover edge cases for the _parse_hostname_string function These tests cover cases where the path supplied does not match the expected path or the file is empty. These test cases highlighted bugs introduced from past commits, which are also fixed in this commit. --- dissect/target/plugins/os/unix/_os.py | 8 ++++-- tests/plugins/os/unix/test__os.py | 41 +++++++++++---------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/dissect/target/plugins/os/unix/_os.py b/dissect/target/plugins/os/unix/_os.py index 5d7658dc7..5a0017b77 100644 --- a/dissect/target/plugins/os/unix/_os.py +++ b/dissect/target/plugins/os/unix/_os.py @@ -133,6 +133,7 @@ def os(self) -> str: return OperatingSystem.UNIX.value def _parse_rh_legacy(self, path): + hostname = None file_contents = path.open("rt").readlines() for line in file_contents: if not line.startswith("HOSTNAME"): @@ -149,7 +150,6 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[ """ redhat_legacy_path = "/etc/sysconfig/network" paths = paths or ["/etc/hostname", "/etc/HOSTNAME", redhat_legacy_path] - hostname_string = None hostname_dict = {"hostname": None, "domain": None} for path in paths: @@ -166,9 +166,11 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[ if hostname_string and "." in hostname_string: hostname_string = hostname_string.split(".", maxsplit=1) hostname_dict = {"hostname": hostname_string[0], "domain": hostname_string[1]} - else: + elif hostname_string != "": hostname_dict = {"hostname": hostname_string, "domain": None} - break + else: + hostname_dict = {"hostname": None, "domain": None} + break # break whenever a valid hostname is found return hostname_dict def _parse_hosts_string(self, paths: Optional[list[str]] = None) -> dict[str, str]: diff --git a/tests/plugins/os/unix/test__os.py b/tests/plugins/os/unix/test__os.py index 69061a8b8..670467b17 100644 --- a/tests/plugins/os/unix/test__os.py +++ b/tests/plugins/os/unix/test__os.py @@ -70,39 +70,32 @@ def test_parse_fstab(tmp_path): @pytest.mark.parametrize( - "path, is_domain_joined, expected_hostname, expected_domain", + "path, expected_hostname, expected_domain, file_content", [ - ("/etc/hostname", True, "myhost", "mydomain.com"), - ("/etc/HOSTNAME", True, "myhost", "mydomain.com"), - ("/etc/sysconfig/network", True, "myhost", "mydomain.com"), - ("/etc/hostname", False, "myhost", None), + ("/etc/hostname", "myhost", "mydomain.com", "myhost.mydomain.com"), + ("/etc/HOSTNAME", "myhost", "mydomain.com", "myhost.mydomain.com"), + ( + "/etc/sysconfig/network", + "myhost", + "mydomain.com", + "NETWORKING=NO\nHOSTNAME=myhost.mydomain.com\nGATEWAY=192.168.1.1", + ), + ("/etc/hostname", "myhost", None, "myhost"), + ("/etc/sysconfig/network", "myhost", None, "NETWORKING=NO\nHOSTNAME=myhost\nGATEWAY=192.168.1.1"), + ("/not_a_valid_hostname_path", None, None, ""), + ("/etc/hostname", None, None, ""), + ("/etc/sysconfig/network", None, None, ""), ], ) def test__parse_hostname_string( target_unix: Target, fs_unix: VirtualFilesystem, path: Path, - is_domain_joined: bool, expected_hostname: str, expected_domain: str, -): - if is_domain_joined: - hostname = "myhost.mydomain.com" - else: - hostname = "myhost" - - if path == "/etc/sysconfig/network": - hostname_file_content = dedent( - f""" - NETWORKING=NO - HOSTNAME={hostname} - GATEWAY=192.168.1.1""" - ) - hostname_file_content = f"HOSTNAME={hostname}" - else: - hostname_file_content = hostname - - fs_unix.map_file_fh(path, BytesIO(hostname_file_content.encode("ascii"))) + file_content: str, +) -> None: + fs_unix.map_file_fh(path, BytesIO(file_content.encode("ascii"))) hostname_dict = target_unix._os._parse_hostname_string() From 928bf21dfde585534ba7c7364f72eb6edb57ee8b Mon Sep 17 00:00:00 2001 From: Floris van Stal Date: Mon, 29 Jan 2024 16:51:57 +0100 Subject: [PATCH 7/7] Remove unused import in test__os.py --- tests/plugins/os/unix/test__os.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/plugins/os/unix/test__os.py b/tests/plugins/os/unix/test__os.py index 670467b17..c727d78ed 100644 --- a/tests/plugins/os/unix/test__os.py +++ b/tests/plugins/os/unix/test__os.py @@ -1,7 +1,6 @@ import tempfile from io import BytesIO from pathlib import Path -from textwrap import dedent from uuid import UUID import pytest