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 +2 -2
- dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
- dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/METADATA +1 -1
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/RECORD +10 -9
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/LICENSE +0 -0
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/WHEEL +0 -0
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/top_level.txt +0 -0
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["
|
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
|
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
|
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
|
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.
|
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
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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 =
|
108
|
-
user_home = nstmp_home if
|
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
|
-
|
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
|
-
|
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.
|
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
|
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=
|
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.
|
322
|
-
dissect.target-3.15.
|
323
|
-
dissect.target-3.15.
|
324
|
-
dissect.target-3.15.
|
325
|
-
dissect.target-3.15.
|
326
|
-
dissect.target-3.15.
|
327
|
-
dissect.target-3.15.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.15.dev29.dist-info → dissect.target-3.15.dev31.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|