dissect.target 3.15.dev29__py3-none-any.whl → 3.15.dev31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dissect/target/plugin.py CHANGED
@@ -495,7 +495,7 @@ def register(plugincls: Type[Plugin]) -> None:
495
495
  root["exports"] = plugincls.__exports__
496
496
  root["namespace"] = plugincls.__namespace__
497
497
  root["fullname"] = ".".join((plugincls.__module__, plugincls.__qualname__))
498
- root["cls"] = plugincls
498
+ root["is_osplugin"] = issubclass(plugincls, OSPlugin)
499
499
 
500
500
 
501
501
  def internal(*args, **kwargs) -> Callable:
@@ -1117,7 +1117,7 @@ def plugin_function_index(target: Optional[Target]) -> tuple[dict[str, PluginDes
1117
1117
  available["exports"].remove("get_all_records")
1118
1118
 
1119
1119
  for exported in available["exports"]:
1120
- if issubclass(available["cls"], OSPlugin) and os_type == general.default.DefaultPlugin:
1120
+ if available["is_osplugin"] and os_type == general.default.DefaultPlugin:
1121
1121
  # This makes the os plugin exports listed under the special
1122
1122
  # "OS plugins" header by the 'plugins' plugin.
1123
1123
  available["module"] = ""
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import re
4
4
  from typing import Iterator, Optional
5
5
 
6
- from dissect.target.filesystem import Filesystem, VirtualFilesystem
6
+ from dissect.target.filesystem import Filesystem
7
7
  from dissect.target.helpers.record import UnixUserRecord
8
8
  from dissect.target.plugin import OperatingSystem, export
9
9
  from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
@@ -18,12 +18,12 @@ RE_CONFIG_USER = re.compile(r"bind system user (?P<user>[^ ]+) ")
18
18
  RE_LOADER_CONFIG_KERNEL_VERSION = re.compile(r'kernel="/(?P<version>.*)"')
19
19
 
20
20
 
21
- class CitrixBsdPlugin(BsdPlugin):
21
+ class CitrixPlugin(BsdPlugin):
22
22
  def __init__(self, target: Target):
23
23
  super().__init__(target)
24
24
  self._ips = []
25
25
  self._hostname = None
26
- self.config_usernames = []
26
+ self._config_usernames = []
27
27
  self._parse_netscaler_configs()
28
28
 
29
29
  def _parse_netscaler_configs(self) -> None:
@@ -49,26 +49,46 @@ class CitrixBsdPlugin(BsdPlugin):
49
49
 
50
50
  @classmethod
51
51
  def detect(cls, target: Target) -> Optional[Filesystem]:
52
- newfilesystem = VirtualFilesystem()
53
- is_citrix = False
52
+ ramdisk = None
53
+ for fs in target.filesystems:
54
+ # /netscaler can be present on both the ramdisk and the harddisk. Therefore we also check for the /log
55
+ # folder, which is not present on the ramdisk. We regard the harddisk as the system volume, as it is
56
+ # possible to only have a disk image of a Netscaler. However, in the case where we only have the ramdisk,
57
+ # we want to fall back on that as the system volume. Thus we store that filesystem in a fallback variable.
58
+ if fs.exists("/netscaler"):
59
+ if fs.exists("/log"):
60
+ return fs
61
+ ramdisk = fs
62
+
63
+ # At this point, we could not find the filesystem for '/var'. Thus, we fall back to the ramdisk variable, which
64
+ # is either 'None' (in which case this isn't a Citrix netscaler), or points to the filesystem of the ramdisk.
65
+ return ramdisk
66
+
67
+ @classmethod
68
+ def create(cls, target: Target, sysvol: Filesystem) -> CitrixPlugin:
69
+ # A disk image of a Citrix Netscaler contains two partitions, that after boot are mounted to /var and /flash.
70
+ # The rest of the filesystem is recreated at runtime into a 'ramdisk'. Currently, this plugin does not
71
+ # yet support recreating the ramdisk from a 'clean' state. This might be possible in a future iteration but
72
+ # requires further research.
73
+
74
+ # When the ramdisk is present within the target's filesystems, mount it accordingly,
54
75
  for fs in target.filesystems:
55
76
  if fs.exists("/bin/freebsd-version"):
56
- newfilesystem.map_fs("/", fs)
57
- break
77
+ # If available, mount the ramdisk first.
78
+ target.fs.mount("/", fs)
79
+ # The 'disk' filesystem is mounted at '/var'.
80
+ target.fs.mount("/var", sysvol)
81
+
82
+ # Enumerate filesystems for flash partition
58
83
  for fs in target.filesystems:
59
84
  if fs.exists("/nsconfig") and fs.exists("/boot"):
60
- newfilesystem.map_fs("/flash", fs)
61
- is_citrix = True
62
- elif fs.exists("/netscaler"):
63
- newfilesystem.map_fs("/var", fs)
64
- is_citrix = True
65
- if is_citrix:
66
- return newfilesystem
67
- return None
85
+ target.fs.mount("/flash", fs)
86
+
87
+ return cls(target)
68
88
 
69
89
  @export(property=True)
70
90
  def hostname(self) -> Optional[str]:
71
- return self._hostname
91
+ return self._hostname or super().hostname
72
92
 
73
93
  @export(property=True)
74
94
  def version(self) -> Optional[str]:
@@ -96,16 +116,21 @@ class CitrixBsdPlugin(BsdPlugin):
96
116
  @export(record=UnixUserRecord)
97
117
  def users(self) -> Iterator[UnixUserRecord]:
98
118
  nstmp_users = set()
99
- nstmp_path = "/var/nstmp/"
100
-
101
- nstmp_user_path = nstmp_path + "{username}"
102
-
103
- for entry in self.target.fs.scandir(nstmp_path):
104
- if entry.is_dir() and entry.name != "#nsinternal#":
105
- nstmp_users.add(entry.name)
119
+ seen = set()
120
+ nstmp_path = self.target.fs.path("/var/nstmp/")
121
+
122
+ # Build a set of nstmp users
123
+ if nstmp_path.exists():
124
+ for entry in nstmp_path.iterdir():
125
+ if entry.is_dir() and entry.name != "#nsinternal#":
126
+ # The nsmonitor user has a home directory of /var/nstmp/monitors rather than /var/nstmp/nsmonitor
127
+ username = "nsmonitor" if entry.name == "monitors" else entry.name
128
+ nstmp_users.add(username)
129
+
130
+ # Yield users from the config, matching them to their 'home' in /var/nstmp if it exists.
106
131
  for username in self._config_usernames:
107
- nstmp_home = nstmp_user_path.format(username=username)
108
- user_home = nstmp_home if self.target.fs.exists(nstmp_home) else None
132
+ nstmp_home = nstmp_path.joinpath(username)
133
+ user_home = nstmp_home if nstmp_home.exists() else None
109
134
 
110
135
  if user_home:
111
136
  # After this loop we will yield all users who are not in the config, but are listed in /var/nstmp/
@@ -114,13 +139,28 @@ class CitrixBsdPlugin(BsdPlugin):
114
139
 
115
140
  if username == "root" and self.target.fs.exists("/root"):
116
141
  # If we got here, 'root' is present both in /var/nstmp and in /root. In such cases, we yield
117
- # the 'root' user as having '/root' as a home, not in /var/nstmp.
118
- user_home = "/root"
142
+ # the 'root' user as having '/root' as a home, not in /var/nstmp, as there is no 'nscli_history'
143
+ # for the root user in /var/nstmp.
144
+ user_home = self.target.fs.path("/root")
119
145
 
146
+ seen.add((username, user_home.as_posix() if user_home else None, None))
120
147
  yield UnixUserRecord(name=username, home=user_home)
121
148
 
149
+ # Yield all users in nstmp that were not observed in the config
122
150
  for username in nstmp_users:
123
- yield UnixUserRecord(name=username, home=nstmp_user_path.format(username=username))
151
+ # The nsmonitor user has a home directory of /var/nstmp/monitors rather than /var/nstmp/nsmonitor
152
+ home = nstmp_path.joinpath(username) if username != "nsmonitor" else nstmp_path.joinpath("monitors")
153
+ seen.add((username, home.as_posix(), None))
154
+ yield UnixUserRecord(name=username, home=home)
155
+
156
+ # Yield users from /etc/passwd if we have not seem them in previous loops
157
+ for user in super().users():
158
+ if (user.name, user.home.as_posix(), user.shell) in seen:
159
+ continue
160
+ # To prevent bogus command history for all users without a home whenever a history is located at the root
161
+ # of the filesystem, we set the user home to None if their home is equivalent to '/'
162
+ user.home = user.home if user.home != "/" else None
163
+ yield user
124
164
 
125
165
  @export(property=True)
126
166
  def os(self) -> str:
@@ -0,0 +1,130 @@
1
+ import re
2
+ from typing import Iterator, Optional
3
+
4
+ from dissect.target.helpers.fsutil import TargetPath
5
+ from dissect.target.helpers.record import UnixUserRecord
6
+ from dissect.target.helpers.utils import year_rollover_helper
7
+ from dissect.target.plugin import export
8
+ from dissect.target.plugins.os.unix.history import (
9
+ CommandHistoryPlugin,
10
+ CommandHistoryRecord,
11
+ )
12
+
13
+ RE_CITRIX_NETSCALER_BASH_HISTORY_DATE = re.compile(r"(?P<date>[^<]+)\s")
14
+
15
+ CITRIX_NETSCALER_BASH_HISTORY_RE = re.compile(
16
+ r"""
17
+ (?P<date>[^<]+)
18
+ \s
19
+ <
20
+ (?P<syslog_facility>[^\.]+)
21
+ \.
22
+ (?P<syslog_loglevel>[^>]+)
23
+ >
24
+ \s
25
+ (?P<hostname>[^\s]+)
26
+ \s
27
+ (?P<process_name>[^\[]+)
28
+ \[
29
+ (?P<process_id>\d+)
30
+ \]
31
+ :
32
+ \s
33
+ (?P<username>.*)\s
34
+ on\s
35
+ (?P<destination>[^\s]+)\s
36
+ shell_command=
37
+ \"
38
+ (?P<command>.*)
39
+ \"
40
+ $
41
+ """,
42
+ re.VERBOSE,
43
+ )
44
+
45
+
46
+ class CitrixCommandHistoryPlugin(CommandHistoryPlugin):
47
+ COMMAND_HISTORY_ABSOLUTE_PATHS = (("citrix-netscaler-bash", "/var/log/bash.log*"),)
48
+ COMMAND_HISTORY_RELATIVE_PATHS = CommandHistoryPlugin.COMMAND_HISTORY_RELATIVE_PATHS + (
49
+ ("citrix-netscaler-cli", ".nscli_history"),
50
+ )
51
+
52
+ def _find_history_files(self) -> list[tuple[str, TargetPath, Optional[UnixUserRecord]]]:
53
+ """Find history files on the target that this plugin can parse."""
54
+ history_files = []
55
+ for shell, history_absolute_path_glob in self.COMMAND_HISTORY_ABSOLUTE_PATHS:
56
+ for path in self.target.fs.path("/").glob(history_absolute_path_glob.lstrip("/")):
57
+ history_files.append((shell, path, None))
58
+
59
+ # Also utilize the _find_history_files function of the parent class
60
+ history_files.extend(super()._find_history_files())
61
+ return history_files
62
+
63
+ def _find_user_by_name(self, username: str) -> Optional[UnixUserRecord]:
64
+ """Cached function to return the matching UnixUserRecord for a given username."""
65
+ if username is None:
66
+ return None
67
+
68
+ user_details = self.target.user_details.find(username=username)
69
+ return user_details.user if user_details else None
70
+
71
+ @export(record=CommandHistoryRecord)
72
+ def commandhistory(self) -> Iterator[CommandHistoryRecord]:
73
+ """Return shell history for all users.
74
+
75
+ When using a shell, history of the used commands is kept on the system.
76
+ """
77
+
78
+ for shell, history_path, user in self._history_files:
79
+ if shell == "citrix-netscaler-cli":
80
+ yield from self.parse_netscaler_cli_history(history_path, user)
81
+ elif shell == "citrix-netscaler-bash":
82
+ yield from self.parse_netscaler_bash_history(history_path)
83
+
84
+ def parse_netscaler_bash_history(self, path: TargetPath) -> Iterator[CommandHistoryRecord]:
85
+ """Parse bash.log* contents."""
86
+ for ts, line in year_rollover_helper(path, RE_CITRIX_NETSCALER_BASH_HISTORY_DATE, "%b %d %H:%M:%S "):
87
+ line = line.strip()
88
+ if not line:
89
+ continue
90
+
91
+ match = CITRIX_NETSCALER_BASH_HISTORY_RE.match(line)
92
+ if not match:
93
+ continue
94
+
95
+ group = match.groupdict()
96
+ command = group.get("command")
97
+ user = self._find_user_by_name(group.get("username"))
98
+
99
+ yield CommandHistoryRecord(
100
+ ts=ts,
101
+ command=command,
102
+ shell="citrix-netscaler-bash",
103
+ source=path,
104
+ _target=self.target,
105
+ _user=user,
106
+ )
107
+
108
+ def parse_netscaler_cli_history(
109
+ self, history_file: TargetPath, user: UnixUserRecord
110
+ ) -> Iterator[CommandHistoryRecord]:
111
+ """Parses the history file of the Citrix Netscaler CLI.
112
+
113
+ The only difference compared to generic bash history files is that the first line will start with
114
+ ``_HiStOrY_V2_``, which we will skip.
115
+ """
116
+ for idx, line in enumerate(history_file.open("rt")):
117
+ if not (line := line.strip()):
118
+ continue
119
+
120
+ if idx == 0 and line == "_HiStOrY_V2_":
121
+ continue
122
+
123
+ yield CommandHistoryRecord(
124
+ ts=None,
125
+ command=line,
126
+ shell="citrix-netscaler-cli",
127
+ source=history_file,
128
+ _target=self.target,
129
+ _user=user,
130
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.15.dev29
3
+ Version: 3.15.dev31
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -3,7 +3,7 @@ dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
4
  dissect/target/filesystem.py,sha256=aLkvZMgeah39Nhlscawh77cm2mzFYI9J5h3uT3Rigtc,53876
5
5
  dissect/target/loader.py,sha256=0-LcZNi7S0qsXR7XGtrzxpuCh9BsLcqNR1T15O7SnBM,7257
6
- dissect/target/plugin.py,sha256=-ME1mkgsnVGlgACFWjM_4DyQ230toCMuh6tPJshSLsw,48112
6
+ dissect/target/plugin.py,sha256=_g5RM8GHXFR1oQvjfCOxq0_m5bbgY-kNr2uFK74nSWI,48128
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
8
  dissect/target/target.py,sha256=1mj4VoDmFZ2d8oXWKVQ-zBK-gXzr0lop6ytQ8E-8GH0,32137
9
9
  dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,15801
@@ -185,7 +185,8 @@ dissect/target/plugins/os/unix/packagemanager.py,sha256=Wm2AAJOD_B3FAcZNXgWtSm_Y
185
185
  dissect/target/plugins/os/unix/shadow.py,sha256=TvN04uzFnUttNMZAa6_1XdXSP-8V6ztbZNoetDvfD0w,3535
186
186
  dissect/target/plugins/os/unix/bsd/_os.py,sha256=e5rttTOFOmd7e2HqP9ZZFMEiPLBr-8rfH0XH1IIeroQ,1372
187
187
  dissect/target/plugins/os/unix/bsd/citrix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
188
- dissect/target/plugins/os/unix/bsd/citrix/_os.py,sha256=Y5kTpOJLyko0Q8Tx6DQ5t0tngzpg8ISNer210JoG0pg,5172
188
+ dissect/target/plugins/os/unix/bsd/citrix/_os.py,sha256=u9agLXoMt_k-nARtSJ78_-ScJae4clZhkqFiEVsB9b8,7910
189
+ dissect/target/plugins/os/unix/bsd/citrix/history.py,sha256=cXMA4rZQBsOMwd_aLbXjW_CAEzNnsr2bUZB9cPufnQo,4498
189
190
  dissect/target/plugins/os/unix/bsd/freebsd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
191
  dissect/target/plugins/os/unix/bsd/freebsd/_os.py,sha256=Vqiyn08kv1IioNUwpgtBJ9SToCFhLCsJdpVhl5E7COM,789
191
192
  dissect/target/plugins/os/unix/bsd/ios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -318,10 +319,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
318
319
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
319
320
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
320
321
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
321
- dissect.target-3.15.dev29.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
322
- dissect.target-3.15.dev29.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
323
- dissect.target-3.15.dev29.dist-info/METADATA,sha256=Zxnx68v6hVRI_vLQaSXRRL9r_K7h_ofCjJQWgabrYUs,11113
324
- dissect.target-3.15.dev29.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
325
- dissect.target-3.15.dev29.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
326
- dissect.target-3.15.dev29.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
327
- dissect.target-3.15.dev29.dist-info/RECORD,,
322
+ dissect.target-3.15.dev31.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
323
+ dissect.target-3.15.dev31.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
324
+ dissect.target-3.15.dev31.dist-info/METADATA,sha256=OTRkkYJl8XwDN-U1m0Nh7C9DBRMl8dsy_NPut3PXJ6U,11113
325
+ dissect.target-3.15.dev31.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
326
+ dissect.target-3.15.dev31.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
327
+ dissect.target-3.15.dev31.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
328
+ dissect.target-3.15.dev31.dist-info/RECORD,,