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 +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
|