dissect.target 3.14.dev29__py3-none-any.whl → 3.15__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/containers/ewf.py +1 -1
- dissect/target/containers/vhd.py +5 -2
- dissect/target/filesystem.py +36 -18
- dissect/target/filesystems/dir.py +10 -4
- dissect/target/filesystems/jffs.py +122 -0
- dissect/target/helpers/compat/path_310.py +506 -0
- dissect/target/helpers/compat/path_311.py +539 -0
- dissect/target/helpers/compat/path_312.py +443 -0
- dissect/target/helpers/compat/path_39.py +545 -0
- dissect/target/helpers/compat/path_common.py +223 -0
- dissect/target/helpers/cyber.py +512 -0
- dissect/target/helpers/fsutil.py +128 -666
- dissect/target/helpers/hashutil.py +17 -57
- dissect/target/helpers/keychain.py +9 -3
- dissect/target/helpers/loaderutil.py +1 -1
- dissect/target/helpers/mount.py +47 -4
- dissect/target/helpers/polypath.py +73 -0
- dissect/target/helpers/record_modifier.py +100 -0
- dissect/target/loader.py +2 -1
- dissect/target/loaders/asdf.py +2 -0
- dissect/target/loaders/cyber.py +37 -0
- dissect/target/loaders/log.py +14 -3
- dissect/target/loaders/raw.py +2 -0
- dissect/target/loaders/remote.py +12 -0
- dissect/target/loaders/tar.py +13 -0
- dissect/target/loaders/targetd.py +2 -0
- dissect/target/loaders/velociraptor.py +12 -3
- dissect/target/loaders/vmwarevm.py +2 -0
- dissect/target/plugin.py +272 -143
- dissect/target/plugins/apps/ssh/openssh.py +11 -54
- dissect/target/plugins/apps/ssh/opensshd.py +4 -3
- dissect/target/plugins/apps/ssh/putty.py +236 -0
- dissect/target/plugins/apps/ssh/ssh.py +58 -0
- dissect/target/plugins/apps/vpn/openvpn.py +6 -0
- dissect/target/plugins/apps/webserver/apache.py +309 -95
- dissect/target/plugins/apps/webserver/caddy.py +5 -2
- dissect/target/plugins/apps/webserver/citrix.py +82 -0
- dissect/target/plugins/apps/webserver/iis.py +9 -12
- dissect/target/plugins/apps/webserver/nginx.py +5 -2
- dissect/target/plugins/apps/webserver/webserver.py +25 -41
- dissect/target/plugins/child/wsl.py +1 -1
- dissect/target/plugins/filesystem/ntfs/mft.py +10 -0
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +10 -0
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +10 -0
- dissect/target/plugins/filesystem/ntfs/utils.py +28 -5
- dissect/target/plugins/filesystem/resolver.py +6 -4
- dissect/target/plugins/general/default.py +0 -2
- dissect/target/plugins/general/example.py +0 -1
- dissect/target/plugins/general/loaders.py +3 -5
- dissect/target/plugins/os/unix/_os.py +3 -3
- dissect/target/plugins/os/unix/bsd/citrix/_os.py +68 -28
- dissect/target/plugins/os/unix/bsd/citrix/history.py +130 -0
- dissect/target/plugins/os/unix/generic.py +17 -12
- dissect/target/plugins/os/unix/linux/fortios/__init__.py +0 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +534 -0
- dissect/target/plugins/os/unix/linux/fortios/generic.py +30 -0
- dissect/target/plugins/os/unix/linux/fortios/locale.py +109 -0
- dissect/target/plugins/os/windows/log/evt.py +1 -1
- dissect/target/plugins/os/windows/log/schedlgu.py +155 -0
- dissect/target/plugins/os/windows/regf/firewall.py +1 -1
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
- dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
- dissect/target/plugins/os/windows/registry.py +1 -1
- dissect/target/plugins/os/windows/sam.py +3 -0
- dissect/target/plugins/os/windows/sru.py +41 -28
- dissect/target/plugins/os/windows/tasks.py +5 -2
- dissect/target/target.py +7 -3
- dissect/target/tools/dd.py +7 -1
- dissect/target/tools/fs.py +8 -1
- dissect/target/tools/info.py +22 -16
- dissect/target/tools/mount.py +28 -3
- dissect/target/tools/query.py +146 -117
- dissect/target/tools/reg.py +21 -16
- dissect/target/tools/shell.py +30 -6
- dissect/target/tools/utils.py +28 -0
- dissect/target/volumes/bde.py +14 -10
- dissect/target/volumes/luks.py +18 -10
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/METADATA +4 -3
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/RECORD +85 -67
- dissect/target/plugins/os/unix/linux/fortigate/_os.py +0 -175
- /dissect/target/{plugins/os/unix/linux/fortigate → helpers/compat}/__init__.py +0 -0
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/LICENSE +0 -0
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/WHEEL +0 -0
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.14.dev29.dist-info → dissect.target-3.15.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,16 @@
|
|
1
|
-
from typing import Iterator
|
1
|
+
from typing import Iterator, Union
|
2
2
|
|
3
|
-
from dissect.target.exceptions import UnsupportedPluginError
|
4
3
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
5
|
-
from dissect.target.plugin import
|
6
|
-
from dissect.target.target import Target
|
4
|
+
from dissect.target.plugin import NamespacePlugin, export
|
7
5
|
|
8
6
|
WebserverAccessLogRecord = TargetRecordDescriptor(
|
9
|
-
"application/log/webserver",
|
7
|
+
"application/log/webserver/access",
|
10
8
|
[
|
11
9
|
("datetime", "ts"),
|
12
10
|
("string", "remote_user"),
|
13
11
|
("net.ipaddress", "remote_ip"),
|
12
|
+
("net.ipaddress", "local_ip"),
|
13
|
+
("varint", "pid"),
|
14
14
|
("string", "method"),
|
15
15
|
("uri", "uri"),
|
16
16
|
("string", "protocol"),
|
@@ -18,49 +18,33 @@ WebserverAccessLogRecord = TargetRecordDescriptor(
|
|
18
18
|
("varint", "bytes_sent"),
|
19
19
|
("uri", "referer"),
|
20
20
|
("string", "useragent"),
|
21
|
+
("varint", "response_time_ms"),
|
21
22
|
("path", "source"),
|
22
23
|
],
|
23
24
|
)
|
24
25
|
|
26
|
+
WebserverErrorLogRecord = TargetRecordDescriptor(
|
27
|
+
"application/log/webserver/error",
|
28
|
+
[
|
29
|
+
("datetime", "ts"),
|
30
|
+
("net.ipaddress", "remote_ip"),
|
31
|
+
("varint", "pid"),
|
32
|
+
("string", "module"),
|
33
|
+
("string", "level"),
|
34
|
+
("string", "error_source"),
|
35
|
+
("string", "error_code"),
|
36
|
+
("string", "message"),
|
37
|
+
("path", "source"),
|
38
|
+
],
|
39
|
+
)
|
25
40
|
|
26
|
-
|
41
|
+
|
42
|
+
class WebserverPlugin(NamespacePlugin):
|
27
43
|
__namespace__ = "webserver"
|
28
44
|
__findable__ = False
|
29
45
|
|
30
|
-
|
31
|
-
|
32
|
-
"nginx",
|
33
|
-
"iis",
|
34
|
-
"caddy",
|
35
|
-
]
|
36
|
-
|
37
|
-
def __init__(self, target: Target):
|
38
|
-
super().__init__(target)
|
39
|
-
self._plugins = []
|
40
|
-
for entry in self.WEBSERVERS:
|
41
|
-
try:
|
42
|
-
self._plugins.append(getattr(self.target, entry))
|
43
|
-
except Exception: # noqa
|
44
|
-
target.log.exception("Failed to load webserver plugin: %s", entry)
|
45
|
-
|
46
|
-
def check_compatible(self) -> None:
|
47
|
-
if not len(self._plugins):
|
48
|
-
raise UnsupportedPluginError("No compatible webserver plugins found")
|
49
|
-
|
50
|
-
def _func(self, f: str) -> Iterator[WebserverAccessLogRecord]:
|
51
|
-
for p in self._plugins:
|
52
|
-
try:
|
53
|
-
yield from getattr(p, f)()
|
54
|
-
except Exception:
|
55
|
-
self.target.log.exception("Failed to execute webserver plugin: %s.%s", p._name, f)
|
56
|
-
|
57
|
-
@export(record=WebserverAccessLogRecord)
|
58
|
-
def logs(self) -> Iterator[WebserverAccessLogRecord]:
|
46
|
+
@export(record=[WebserverAccessLogRecord, WebserverErrorLogRecord])
|
47
|
+
def logs(self) -> Iterator[Union[WebserverAccessLogRecord, WebserverErrorLogRecord]]:
|
59
48
|
"""Returns log file records from installed webservers."""
|
60
49
|
yield from self.access()
|
61
|
-
|
62
|
-
|
63
|
-
@export(record=WebserverAccessLogRecord)
|
64
|
-
def access(self) -> Iterator[WebserverAccessLogRecord]:
|
65
|
-
"""Returns WebserverAccessLogRecord records from installed webservers."""
|
66
|
-
yield from self._func("access")
|
50
|
+
yield from self.error()
|
@@ -26,7 +26,7 @@ def find_wsl_installs(target: Target) -> Iterator[Path]:
|
|
26
26
|
continue
|
27
27
|
base_path = target.resolve(distribution_key.value("BasePath").value)
|
28
28
|
# WSL needs diskname to be ext4.vhdx, but they can be renamed when WSL is not active
|
29
|
-
yield from
|
29
|
+
yield from base_path.glob("*.vhdx")
|
30
30
|
except PluginError:
|
31
31
|
pass
|
32
32
|
|
@@ -123,6 +123,12 @@ class MftPlugin(Plugin):
|
|
123
123
|
|
124
124
|
The Master File Table (MFT) contains primarily metadata about every file and folder on a NFTS filesystem.
|
125
125
|
|
126
|
+
If the filesystem is part of a virtual NTFS filesystem (a ``VirtualFilesystem`` with the MFT properties
|
127
|
+
added to it through a "fake" ``NtfsFilesystem``), the paths returned in the MFT records are based on the
|
128
|
+
mount point of the ``VirtualFilesystem``. This ensures that the proper original drive letter is used when
|
129
|
+
available.
|
130
|
+
When no drive letter can be determined, the path will show as e.g. ``\\$fs$\\fs0``.
|
131
|
+
|
126
132
|
References:
|
127
133
|
- https://docs.microsoft.com/en-us/windows/win32/fileio/master-file-table
|
128
134
|
"""
|
@@ -136,6 +142,10 @@ class MftPlugin(Plugin):
|
|
136
142
|
if fs.__type__ != "ntfs":
|
137
143
|
continue
|
138
144
|
|
145
|
+
# If this filesystem is a "fake" NTFS filesystem, used to enhance a
|
146
|
+
# VirtualFilesystem, The driveletter (more accurate mount point)
|
147
|
+
# returned will be that of the VirtualFilesystem. This makes sure
|
148
|
+
# the paths returned in the records are actually reachable.
|
139
149
|
drive_letter = get_drive_letter(self.target, fs)
|
140
150
|
volume_uuid = get_volume_identifier(fs)
|
141
151
|
|
@@ -105,6 +105,12 @@ class MftTimelinePlugin(Plugin):
|
|
105
105
|
|
106
106
|
The Master File Table (MFT) contains metadata about every file and folder on a NFTS filesystem.
|
107
107
|
|
108
|
+
If the filesystem is part of a virtual NTFS filesystem (a ``VirtualFilesystem`` with the MFT properties
|
109
|
+
added to it through a "fake" ``NtfsFilesystem``), the paths returned in the MFT records are based on the
|
110
|
+
mount point of the ``VirtualFilesystem``. This ensures that the proper original drive letter is used when
|
111
|
+
available.
|
112
|
+
When no drive letter can be determined, the path will show as e.g. ``\\$fs$\\fs0``.
|
113
|
+
|
108
114
|
References:
|
109
115
|
- https://docs.microsoft.com/en-us/windows/win32/fileio/master-file-table
|
110
116
|
"""
|
@@ -112,6 +118,10 @@ class MftTimelinePlugin(Plugin):
|
|
112
118
|
if fs.__type__ != "ntfs":
|
113
119
|
continue
|
114
120
|
|
121
|
+
# If this filesystem is a "fake" NTFS filesystem, used to enhance a
|
122
|
+
# VirtualFilesystem, The driveletter (more accurate mount point)
|
123
|
+
# returned will be that of the VirtualFilesystem. This makes sure
|
124
|
+
# the paths returned in the records are actually reachable.
|
115
125
|
drive_letter = get_drive_letter(self.target, fs)
|
116
126
|
extras = Extras(
|
117
127
|
serial=fs.ntfs.serial,
|
@@ -34,6 +34,12 @@ class UsnjrnlPlugin(Plugin):
|
|
34
34
|
The Update Sequence Number Journal (UsnJrnl) is a feature of an NTFS file system and contains information about
|
35
35
|
filesystem activities. Each volume has its own UsnJrnl.
|
36
36
|
|
37
|
+
If the filesystem is part of a virtual NTFS filesystem (a ``VirtualFilesystem`` with the UsnJrnl
|
38
|
+
properties added to it through a "fake" ``NtfsFilesystem``), the paths returned in the UsnJrnl records
|
39
|
+
are based on the mount point of the ``VirtualFilesystem``. This ensures that the proper original drive
|
40
|
+
letter is used when available.
|
41
|
+
When no drive letter can be determined, the path will show as e.g. ``\\$fs$\\fs0``.
|
42
|
+
|
37
43
|
References:
|
38
44
|
- https://en.wikipedia.org/wiki/USN_Journal
|
39
45
|
- https://velociraptor.velocidex.com/the-windows-usn-journal-f0c55c9010e
|
@@ -47,6 +53,10 @@ class UsnjrnlPlugin(Plugin):
|
|
47
53
|
if not usnjrnl:
|
48
54
|
continue
|
49
55
|
|
56
|
+
# If this filesystem is a "fake" NTFS filesystem, used to enhance a
|
57
|
+
# VirtualFilesystem, The driveletter (more accurate mount point)
|
58
|
+
# returned will be that of the VirtualFilesystem. This makes sure
|
59
|
+
# the paths returned in the records are actually reachable.
|
50
60
|
drive_letter = get_drive_letter(self.target, fs)
|
51
61
|
for record in usnjrnl.records():
|
52
62
|
try:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import re
|
1
2
|
from enum import Enum, auto
|
2
3
|
from typing import Optional, Tuple
|
3
4
|
from uuid import UUID
|
@@ -8,6 +9,8 @@ from dissect.ntfs.mft import MftRecord
|
|
8
9
|
from dissect.target import Target
|
9
10
|
from dissect.target.filesystems.ntfs import NtfsFilesystem
|
10
11
|
|
12
|
+
DRIVE_LETTER_RE = re.compile(r"[a-zA-Z]:")
|
13
|
+
|
11
14
|
|
12
15
|
class InformationType(Enum):
|
13
16
|
STANDARD_INFORMATION = auto()
|
@@ -20,13 +23,33 @@ def get_drive_letter(target: Target, filesystem: NtfsFilesystem):
|
|
20
23
|
|
21
24
|
When the drive letter is not available for that filesystem it returns empty.
|
22
25
|
"""
|
26
|
+
# A filesystem can be known under multiple drives (mount points). If it is
|
27
|
+
# a windows system volume, there are the default sysvol and c: drives.
|
28
|
+
# If the target has a virtual ntfs filesystem, e.g. as constructed by the
|
29
|
+
# tar and dir loaders, there is also the /$fs$/fs<n> drive, under which the
|
30
|
+
# "fake" ntfs filesystem is mounted.
|
31
|
+
# The precedence for drives is first the drive letter drives (e.g. c:),
|
32
|
+
# second the "normally" named drives (e.g. sysvol) and finally the anonymous
|
33
|
+
# drives (e.g. /$fs/fs0).
|
23
34
|
mount_items = (item for item in target.fs.mounts.items() if hasattr(item[1], "ntfs"))
|
24
|
-
|
35
|
+
drives = [key for key, fs in mount_items if fs.ntfs is filesystem.ntfs]
|
36
|
+
|
37
|
+
single_letter_drives = []
|
38
|
+
other_drives = []
|
39
|
+
anon_drives = []
|
40
|
+
|
41
|
+
for drive in drives:
|
42
|
+
if DRIVE_LETTER_RE.match(drive):
|
43
|
+
single_letter_drives.append(drive)
|
44
|
+
elif "$fs$" in drive:
|
45
|
+
anon_drives.append(drive)
|
46
|
+
else:
|
47
|
+
other_drives.append(drive)
|
48
|
+
|
49
|
+
drives = sorted(single_letter_drives) + sorted(other_drives) + sorted(anon_drives)
|
25
50
|
|
26
|
-
if
|
27
|
-
|
28
|
-
# This is to choose the latter which will be 'c:'
|
29
|
-
return f"{driveletters[-1]}\\"
|
51
|
+
if drives:
|
52
|
+
return f"{drives[0]}\\"
|
30
53
|
else:
|
31
54
|
return ""
|
32
55
|
|
@@ -2,7 +2,7 @@ import re
|
|
2
2
|
from typing import Optional
|
3
3
|
|
4
4
|
from dissect.target.helpers import fsutil
|
5
|
-
from dissect.target.plugin import Plugin, internal
|
5
|
+
from dissect.target.plugin import OperatingSystem, Plugin, internal
|
6
6
|
|
7
7
|
re_quoted = re.compile(r"\"(.+?)\"")
|
8
8
|
|
@@ -34,10 +34,12 @@ class ResolverPlugin(Plugin):
|
|
34
34
|
if not path:
|
35
35
|
return path
|
36
36
|
|
37
|
-
if self.target.os ==
|
38
|
-
|
37
|
+
if self.target.os == OperatingSystem.WINDOWS:
|
38
|
+
resolved_path = self.resolve_windows(path, user_sid=user)
|
39
39
|
else:
|
40
|
-
|
40
|
+
resolved_path = self.resolve_default(path, user_id=user)
|
41
|
+
|
42
|
+
return self.target.fs.path(resolved_path)
|
41
43
|
|
42
44
|
def resolve_windows(self, path: str, user_sid: Optional[str] = None) -> str:
|
43
45
|
# Normalize first so the replacements are easier
|
@@ -1,7 +1,5 @@
|
|
1
|
-
import itertools
|
2
|
-
|
3
1
|
from dissect.target.helpers.docs import INDENT_STEP, get_docstring
|
4
|
-
from dissect.target.loader import
|
2
|
+
from dissect.target.loader import LOADERS_BY_SCHEME
|
5
3
|
from dissect.target.plugin import Plugin, export
|
6
4
|
|
7
5
|
|
@@ -16,10 +14,10 @@ class LoaderListPlugin(Plugin):
|
|
16
14
|
"""List the available loaders."""
|
17
15
|
|
18
16
|
loaders_info = {}
|
19
|
-
for loader in
|
17
|
+
for key, loader in LOADERS_BY_SCHEME.items():
|
20
18
|
try:
|
21
19
|
docstring = get_docstring(loader, "No documentation.").splitlines()[0].strip()
|
22
|
-
loaders_info[
|
20
|
+
loaders_info[key] = docstring
|
23
21
|
except ImportError:
|
24
22
|
continue
|
25
23
|
|
@@ -254,7 +254,7 @@ class UnixPlugin(OSPlugin):
|
|
254
254
|
continue
|
255
255
|
return os_release
|
256
256
|
|
257
|
-
def _get_architecture(self, os: str = "unix") -> Optional[str]:
|
257
|
+
def _get_architecture(self, os: str = "unix", path: str = "/bin/ls") -> Optional[str]:
|
258
258
|
arch_strings = {
|
259
259
|
0x00: "Unknown",
|
260
260
|
0x02: "SPARC",
|
@@ -271,8 +271,8 @@ class UnixPlugin(OSPlugin):
|
|
271
271
|
}
|
272
272
|
|
273
273
|
for fs in self.target.filesystems:
|
274
|
-
if fs.exists(
|
275
|
-
fh = fs.open(
|
274
|
+
if fs.exists(path):
|
275
|
+
fh = fs.open(path)
|
276
276
|
fh.seek(4)
|
277
277
|
# ELF - e_ident[EI_CLASS]
|
278
278
|
bits = unpack("B", fh.read(1))[0]
|
@@ -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,4 +1,5 @@
|
|
1
1
|
from datetime import datetime
|
2
|
+
from pathlib import Path
|
2
3
|
from statistics import median
|
3
4
|
from typing import Optional
|
4
5
|
|
@@ -15,18 +16,7 @@ class GenericPlugin(Plugin):
|
|
15
16
|
def activity(self) -> Optional[datetime]:
|
16
17
|
"""Return last seen activity based on filesystem timestamps."""
|
17
18
|
var_log = self.target.fs.path("/var/log")
|
18
|
-
|
19
|
-
return
|
20
|
-
|
21
|
-
last_seen = 0
|
22
|
-
for f in var_log.iterdir():
|
23
|
-
if not f.exists():
|
24
|
-
continue
|
25
|
-
if f.stat().st_mtime > last_seen:
|
26
|
-
last_seen = f.stat().st_mtime
|
27
|
-
|
28
|
-
if last_seen != 0:
|
29
|
-
return ts.from_unix(last_seen)
|
19
|
+
return calculate_last_activity(var_log)
|
30
20
|
|
31
21
|
@export(property=True)
|
32
22
|
def install_date(self) -> Optional[datetime]:
|
@@ -63,3 +53,18 @@ class GenericPlugin(Plugin):
|
|
63
53
|
root_stat = self.target.fs.stat("/")
|
64
54
|
if root_stat.st_ctime == root_stat.st_mtime:
|
65
55
|
return ts.from_unix(root_stat.st_ctime)
|
56
|
+
|
57
|
+
|
58
|
+
def calculate_last_activity(folder: Path) -> Optional[datetime]:
|
59
|
+
if not folder.exists():
|
60
|
+
return
|
61
|
+
|
62
|
+
last_seen = 0
|
63
|
+
for file in folder.iterdir():
|
64
|
+
if not file.exists():
|
65
|
+
continue
|
66
|
+
if file.stat().st_mtime > last_seen:
|
67
|
+
last_seen = file.stat().st_mtime
|
68
|
+
|
69
|
+
if last_seen != 0:
|
70
|
+
return ts.from_unix(last_seen)
|
File without changes
|