Skip to content

Commit 45bacf3

Browse files
authored
Explicitly mark home directories as having a posix or windows flavor (#892)
- Mark home directories of users as either posix_path or windows_path - Add missing unit test for parsing of users on windows and unix
1 parent 76dfe96 commit 45bacf3

File tree

7 files changed

+69
-31
lines changed

7 files changed

+69
-31
lines changed

dissect/target/plugins/os/unix/_os.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import re
55
import uuid
66
from struct import unpack
7-
from typing import Iterator, Optional, Union
7+
from typing import Iterator
8+
9+
from flow.record.fieldtypes import posix_path
810

911
from dissect.target.filesystem import Filesystem
1012
from dissect.target.helpers.fsutil import TargetPath
@@ -25,7 +27,7 @@ def __init__(self, target: Target):
2527
self._os_release = self._parse_os_release()
2628

2729
@classmethod
28-
def detect(cls, target: Target) -> Optional[Filesystem]:
30+
def detect(cls, target: Target) -> Filesystem | None:
2931
for fs in target.filesystems:
3032
if fs.exists("/var") and fs.exists("/etc"):
3133
return fs
@@ -71,7 +73,7 @@ def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
7173
uid=pwent.get(2),
7274
gid=pwent.get(3),
7375
gecos=pwent.get(4),
74-
home=self.target.fs.path(pwent.get(5)),
76+
home=posix_path(pwent.get(5)),
7577
shell=pwent.get(6),
7678
source=passwd_file,
7779
_target=self.target,
@@ -115,23 +117,23 @@ def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
115117

116118
yield UnixUserRecord(
117119
name=user["name"],
118-
home=user["home"],
120+
home=posix_path(user["home"]),
119121
shell=user["shell"],
120122
source="/var/log/syslog",
121123
_target=self.target,
122124
)
123125

124126
@export(property=True)
125-
def architecture(self) -> Optional[str]:
127+
def architecture(self) -> str | None:
126128
return self._get_architecture(self.os)
127129

128130
@export(property=True)
129-
def hostname(self) -> Optional[str]:
131+
def hostname(self) -> str | None:
130132
hosts_string = self._hosts_dict.get("hostname", "localhost")
131133
return self._hostname_dict.get("hostname", hosts_string)
132134

133135
@export(property=True)
134-
def domain(self) -> Optional[str]:
136+
def domain(self) -> str | None:
135137
domain = self._hostname_dict.get("domain", "localhost")
136138
if domain == "localhost":
137139
domain = self._hosts_dict["hostname", "localhost"]
@@ -152,7 +154,7 @@ def _parse_rh_legacy(self, path):
152154
_, _, hostname = line.rstrip().partition("=")
153155
return hostname
154156

155-
def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[dict[str, str]]:
157+
def _parse_hostname_string(self, paths: list[str] | None = None) -> dict[str, str] | None:
156158
"""
157159
Returns a dict containing the hostname and domain name portion of the path(s) specified
158160
@@ -184,7 +186,7 @@ def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[
184186
break # break whenever a valid hostname is found
185187
return hostname_dict
186188

187-
def _parse_hosts_string(self, paths: Optional[list[str]] = None) -> dict[str, str]:
189+
def _parse_hosts_string(self, paths: list[str] | None = None) -> dict[str, str]:
188190
paths = paths or ["/etc/hosts"]
189191
hosts_string = {"ip": None, "hostname": None}
190192

@@ -244,7 +246,7 @@ def _add_mounts(self) -> None:
244246
self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
245247
self.target.fs.mount(mount_point, fs)
246248

247-
def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
249+
def _parse_os_release(self, glob: str | None = None) -> dict[str, str]:
248250
"""Parse files containing Unix version information.
249251
250252
Not all these files are equal. Generally speaking these files are
@@ -286,7 +288,7 @@ def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
286288
continue
287289
return os_release
288290

289-
def _get_architecture(self, os: str = "unix", path: str = "/bin/ls") -> Optional[str]:
291+
def _get_architecture(self, os: str = "unix", path: str = "/bin/ls") -> str | None:
290292
arch_strings = {
291293
0x00: "Unknown",
292294
0x02: "SPARC",
@@ -322,7 +324,7 @@ def _get_architecture(self, os: str = "unix", path: str = "/bin/ls") -> Optional
322324
def parse_fstab(
323325
fstab: TargetPath,
324326
log: logging.Logger = log,
325-
) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str, str]]:
327+
) -> Iterator[tuple[uuid.UUID | str, str, str, str, str]]:
326328
"""Parse fstab file and return a generator that streams the details of entries,
327329
with unsupported FS types and block devices filtered away.
328330
"""

dissect/target/plugins/os/windows/_os.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import operator
44
import struct
5-
from typing import Any, Iterator, Optional
5+
from typing import Any, Iterator
6+
7+
from flow.record.fieldtypes import windows_path
68

79
from dissect.target.exceptions import RegistryError, RegistryValueNotFoundError
810
from dissect.target.filesystem import Filesystem
@@ -26,7 +28,7 @@ def __init__(self, target: Target):
2628
)
2729

2830
@classmethod
29-
def detect(cls, target: Target) -> Optional[Filesystem]:
31+
def detect(cls, target: Target) -> Filesystem | None:
3032
for fs in target.filesystems:
3133
if fs.exists("/windows/system32") or fs.exists("/winnt"):
3234
return fs
@@ -90,7 +92,7 @@ def add_mounts(self) -> None:
9092
self.target.log.warning("Unknown drive letter for sysvol")
9193

9294
@export(property=True)
93-
def hostname(self) -> Optional[str]:
95+
def hostname(self) -> str | None:
9496
key = "HKLM\\SYSTEM\\ControlSet001\\Control\\Computername\\Computername"
9597
try:
9698
return self.target.registry.value(key, "Computername").value
@@ -109,7 +111,7 @@ def _get_version_reg_value(self, value_name: str) -> Any:
109111

110112
return value
111113

112-
def _legacy_current_version(self) -> Optional[str]:
114+
def _legacy_current_version(self) -> str | None:
113115
"""Returns the NT version as used up to and including NT 6.3.
114116
115117
This corresponds with Windows 8 / Windows 2012 Server.
@@ -121,7 +123,7 @@ def _legacy_current_version(self) -> Optional[str]:
121123
"""
122124
return self._get_version_reg_value("CurrentVersion")
123125

124-
def _major_version(self) -> Optional[int]:
126+
def _major_version(self) -> int | None:
125127
"""Return the NT major version number (starting from NT 10.0 / Windows 10).
126128
127129
Returns:
@@ -131,7 +133,7 @@ def _major_version(self) -> Optional[int]:
131133
"""
132134
return self._get_version_reg_value("CurrentMajorVersionNumber")
133135

134-
def _minor_version(self) -> Optional[int]:
136+
def _minor_version(self) -> int | None:
135137
"""Return the NT minor version number (starting from NT 10.0 / Windows 10).
136138
137139
Returns:
@@ -141,7 +143,7 @@ def _minor_version(self) -> Optional[int]:
141143
"""
142144
return self._get_version_reg_value("CurrentMinorVersionNumber")
143145

144-
def _nt_version(self) -> Optional[int]:
146+
def _nt_version(self) -> int | None:
145147
"""Return the Windows NT version in x.y format.
146148
147149
For systems up to and including NT 6.3 (Win 8 / Win 2012 Server) this
@@ -169,7 +171,7 @@ def _nt_version(self) -> Optional[int]:
169171
return version
170172

171173
@export(property=True)
172-
def version(self) -> Optional[str]:
174+
def version(self) -> str | None:
173175
"""Return a string representation of the Windows version of the target.
174176
175177
For Windows versions before Windows 10 this looks like::
@@ -255,7 +257,7 @@ def _part_str(parts: dict[str, Any], name: str) -> str:
255257
return version_string
256258

257259
@export(property=True)
258-
def architecture(self) -> Optional[str]:
260+
def architecture(self) -> str | None:
259261
"""
260262
Returns a dict containing the architecture and bitness of the system
261263
@@ -312,7 +314,7 @@ def users(self) -> Iterator[WindowsUserRecord]:
312314
yield WindowsUserRecord(
313315
sid=subkey.name,
314316
name=name,
315-
home=home,
317+
home=windows_path(home),
316318
_target=self.target,
317319
)
318320

tests/plugins/os/unix/bsd/citrix/test__os.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import textwrap
22
from io import BytesIO
33

4+
from flow.record.fieldtypes import posix_path
5+
46
from dissect.target.filesystem import VirtualFilesystem
57
from dissect.target.plugins.os.unix.bsd.citrix._os import CitrixPlugin
68
from dissect.target.target import Target
@@ -39,7 +41,7 @@ def test_citrix_os(target_citrix: Target, fs_bsd: VirtualFilesystem) -> None:
3941
assert len(users) == 8
4042

4143
assert users[0].name == "alfred" # Only listed in /var/nstmp
42-
assert users[0].home == "/var/nstmp/alfred"
44+
assert users[0].home == posix_path("/var/nstmp/alfred")
4345

4446
assert users[1].name == "batman" # Only listed in config
4547
assert users[1].home is None
@@ -51,15 +53,15 @@ def test_citrix_os(target_citrix: Target, fs_bsd: VirtualFilesystem) -> None:
5153
assert users[3].home is None
5254

5355
assert users[4].name == "nobody" # User entry for the nobody user from /etc/passwd
54-
assert users[4].home == "/nonexistent"
56+
assert users[4].home == posix_path("/nonexistent")
5557

5658
assert users[5].name == "robin" # Listed in config and /var/nstmp
57-
assert users[5].home == "/var/nstmp/robin"
59+
assert users[5].home == posix_path("/var/nstmp/robin")
5860

5961
assert users[6].name == "root" # User entry for /root, from the config
60-
assert users[6].home == "/root"
62+
assert users[6].home == posix_path("/root")
6163
assert users[6].shell is None
6264

6365
assert users[7].name == "root" # User entry for /root, from /etc/passwd
64-
assert users[7].home == "/root"
66+
assert users[7].home == posix_path("/root")
6567
assert users[7].shell == "/usr/bin/bash"

tests/plugins/os/unix/bsd/osx/test__os.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def test_unix_bsd_osx_os(target_osx_users, fs_osx):
2727

2828
assert dissect_user.name == "_dissect"
2929
assert dissect_user.passwd == "*"
30-
assert dissect_user.home == "/Users/dissect"
31-
assert isinstance(dissect_user.home, posix_path)
30+
assert dissect_user.home == posix_path("/Users/dissect")
3231
assert dissect_user.shell == "/usr/bin/false"
3332
assert dissect_user.source == "/var/db/dslocal/nodes/Default/users/_dissect.plist"
3433

tests/plugins/os/unix/linux/test_environ.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ def test_environ(target_linux_users: Target, fs_linux_proc: VirtualFilesystem):
77
target_linux_users.add_plugin(ProcPlugin)
88
results = list(target_linux_users.environ())
99
assert len(results) == 4
10-
print(results)

tests/plugins/os/unix/test__os.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from uuid import UUID
66

77
import pytest
8+
from flow.record.fieldtypes import posix_path
89

910
from dissect.target.filesystem import VirtualFilesystem
1011
from dissect.target.plugins.os.unix._os import UnixPlugin, parse_fstab
@@ -113,7 +114,7 @@ def test_mount_volume_name_regression(fs_unix: VirtualFilesystem) -> None:
113114
("/etc/sysconfig/network", None, None, ""),
114115
],
115116
)
116-
def test__parse_hostname_string(
117+
def test_parse_hostname_string(
117118
target_unix: Target,
118119
fs_unix: VirtualFilesystem,
119120
path: Path,
@@ -127,3 +128,21 @@ def test__parse_hostname_string(
127128

128129
assert hostname_dict["hostname"] == expected_hostname
129130
assert hostname_dict["domain"] == expected_domain
131+
132+
133+
def test_users(target_unix_users: Target) -> None:
134+
users = list(target_unix_users.users())
135+
136+
assert len(users) == 2
137+
138+
assert users[0].name == "root"
139+
assert users[0].uid == 0
140+
assert users[0].gid == 0
141+
assert users[0].home == posix_path("/root")
142+
assert users[0].shell == "/bin/bash"
143+
144+
assert users[1].name == "user"
145+
assert users[1].uid == 1000
146+
assert users[1].gid == 1000
147+
assert users[1].home == posix_path("/home/user")
148+
assert users[1].shell == "/bin/bash"

tests/plugins/os/windows/test__os.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Iterator
44

55
import pytest
6+
from flow.record.fieldtypes import windows_path
67

78
from dissect.target.filesystem import Filesystem
89
from dissect.target.helpers.regutil import VirtualKey, VirtualValue
@@ -253,3 +254,17 @@ def test_windows_os_detection_with_linux_folders(target_win_linux_folders: Targe
253254

254255
assert fs_linux is None
255256
assert fs_windows is not None
257+
258+
259+
def test_windows_user(target_win_users: Target) -> None:
260+
users = list(target_win_users.users())
261+
262+
assert len(users) == 2
263+
264+
assert users[0].sid == "S-1-5-18"
265+
assert users[0].name == "systemprofile"
266+
assert users[0].home == windows_path("%systemroot%\\system32\\config\\systemprofile")
267+
268+
assert users[1].sid == "S-1-5-21-3263113198-3007035898-945866154-1002"
269+
assert users[1].name == "John"
270+
assert users[1].home == windows_path("C:\\Users\\John")

0 commit comments

Comments
 (0)