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

Sign up to get free protection for your applications and to get access to all the features.
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,,