dissect.target 3.19.dev58__py3-none-any.whl → 3.20__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/container.py +1 -1
- dissect/target/exceptions.py +6 -5
- dissect/target/filesystem.py +2 -2
- dissect/target/filesystems/btrfs.py +14 -5
- dissect/target/filesystems/config.py +5 -1
- dissect/target/filesystems/extfs.py +5 -4
- dissect/target/filesystems/fat.py +22 -16
- dissect/target/filesystems/ffs.py +11 -4
- dissect/target/filesystems/jffs.py +12 -7
- dissect/target/filesystems/ntfs.py +22 -6
- dissect/target/filesystems/overlay.py +14 -4
- dissect/target/filesystems/smb.py +3 -3
- dissect/target/filesystems/squashfs.py +4 -4
- dissect/target/filesystems/vmfs.py +4 -4
- dissect/target/filesystems/xfs.py +15 -8
- dissect/target/helpers/compat/path_common.py +5 -5
- dissect/target/helpers/configutil.py +128 -32
- dissect/target/helpers/cyber.py +2 -0
- dissect/target/helpers/data/windowsZones.xml +19 -23
- dissect/target/helpers/docs.py +1 -1
- dissect/target/helpers/keychain.py +2 -0
- dissect/target/helpers/mount.py +2 -1
- dissect/target/helpers/record.py +29 -2
- dissect/target/helpers/record_modifier.py +5 -1
- dissect/target/helpers/regutil.py +56 -26
- dissect/target/loader.py +1 -1
- dissect/target/loaders/mqtt.py +104 -9
- dissect/target/loaders/proxmox.py +68 -0
- dissect/target/loaders/vma.py +1 -1
- dissect/target/loaders/xva.py +1 -1
- dissect/target/plugin.py +24 -21
- dissect/target/plugins/apps/av/mcafee.py +2 -0
- dissect/target/plugins/apps/av/sophos.py +2 -0
- dissect/target/plugins/apps/av/trendmicro.py +2 -0
- dissect/target/plugins/apps/browser/chromium.py +27 -6
- dissect/target/plugins/apps/container/docker.py +48 -32
- dissect/target/plugins/apps/editor/__init__.py +0 -0
- dissect/target/plugins/apps/editor/editor.py +23 -0
- dissect/target/plugins/apps/{texteditor → editor}/windowsnotepad.py +40 -31
- dissect/target/plugins/apps/other/__init__.py +0 -0
- dissect/target/plugins/apps/other/env.py +56 -0
- dissect/target/plugins/apps/shell/powershell.py +6 -2
- dissect/target/plugins/apps/shell/wget.py +91 -0
- dissect/target/plugins/apps/ssh/openssh.py +2 -0
- dissect/target/plugins/apps/ssh/opensshd.py +2 -0
- dissect/target/plugins/apps/virtualization/__init__.py +0 -0
- dissect/target/plugins/apps/virtualization/vmware_workstation.py +61 -0
- dissect/target/plugins/apps/vpn/wireguard.py +9 -9
- dissect/target/plugins/apps/webhosting/cpanel.py +2 -0
- dissect/target/plugins/apps/webserver/caddy.py +2 -0
- dissect/target/plugins/apps/webserver/nginx.py +2 -0
- dissect/target/plugins/child/esxi.py +3 -1
- dissect/target/plugins/child/parallels.py +68 -0
- dissect/target/plugins/child/proxmox.py +23 -0
- dissect/target/plugins/child/virtuozzo.py +12 -8
- dissect/target/plugins/child/vmware_workstation.py +23 -8
- dissect/target/plugins/filesystem/acquire_hash.py +2 -1
- dissect/target/plugins/filesystem/icat.py +15 -11
- dissect/target/plugins/filesystem/ntfs/mft.py +10 -6
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +3 -1
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -0
- dissect/target/plugins/filesystem/ntfs/utils.py +3 -1
- dissect/target/plugins/filesystem/unix/suid.py +4 -1
- dissect/target/plugins/filesystem/walkfs.py +2 -0
- dissect/target/plugins/general/example.py +2 -2
- dissect/target/plugins/general/loaders.py +18 -5
- dissect/target/plugins/general/network.py +20 -5
- dissect/target/plugins/general/osinfo.py +1 -0
- dissect/target/plugins/general/plugins.py +53 -10
- dissect/target/plugins/os/unix/_os.py +70 -44
- dissect/target/plugins/os/unix/applications.py +78 -0
- dissect/target/plugins/os/unix/bsd/citrix/history.py +2 -0
- dissect/target/plugins/os/unix/bsd/osx/_os.py +4 -21
- dissect/target/plugins/os/unix/bsd/osx/network.py +92 -0
- dissect/target/plugins/os/unix/bsd/osx/user.py +4 -0
- dissect/target/plugins/os/unix/cronjobs.py +8 -4
- dissect/target/plugins/os/unix/etc/etc.py +4 -0
- dissect/target/plugins/os/unix/generic.py +2 -0
- dissect/target/plugins/os/unix/history.py +27 -25
- dissect/target/plugins/os/unix/linux/_os.py +8 -10
- dissect/target/plugins/os/unix/linux/cmdline.py +2 -0
- dissect/target/plugins/os/unix/linux/debian/apt.py +4 -1
- dissect/target/plugins/os/unix/linux/debian/dpkg.py +3 -3
- dissect/target/plugins/os/unix/linux/debian/proxmox/__init__.py +0 -0
- dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +141 -0
- dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py +29 -0
- dissect/target/plugins/os/unix/linux/debian/snap.py +79 -0
- dissect/target/plugins/os/unix/linux/environ.py +2 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +74 -63
- dissect/target/plugins/os/unix/linux/fortios/generic.py +2 -0
- dissect/target/plugins/os/unix/linux/fortios/locale.py +2 -0
- dissect/target/plugins/os/unix/linux/modules.py +2 -0
- dissect/target/plugins/os/unix/linux/netstat.py +2 -0
- dissect/target/{helpers → plugins/os/unix/linux}/network_managers.py +11 -9
- dissect/target/plugins/os/unix/linux/processes.py +2 -0
- dissect/target/plugins/os/unix/linux/redhat/yum.py +4 -1
- dissect/target/plugins/os/unix/linux/services.py +5 -3
- dissect/target/plugins/os/unix/linux/sockets.py +2 -0
- dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -1
- dissect/target/plugins/os/unix/locale.py +2 -0
- dissect/target/plugins/os/unix/locate/gnulocate.py +4 -2
- dissect/target/plugins/os/unix/locate/mlocate.py +2 -0
- dissect/target/plugins/os/unix/locate/plocate.py +3 -1
- dissect/target/plugins/os/unix/log/atop.py +2 -0
- dissect/target/plugins/os/unix/log/audit.py +3 -1
- dissect/target/plugins/os/unix/log/auth.py +351 -38
- dissect/target/plugins/os/unix/log/journal.py +123 -101
- dissect/target/plugins/os/unix/log/lastlog.py +5 -3
- dissect/target/plugins/os/unix/log/messages.py +51 -27
- dissect/target/plugins/os/unix/log/utmp.py +52 -71
- dissect/target/plugins/os/unix/packagemanager.py +5 -38
- dissect/target/plugins/os/unix/shadow.py +3 -1
- dissect/target/plugins/os/unix/trash.py +132 -0
- dissect/target/plugins/os/windows/_os.py +22 -41
- dissect/target/plugins/os/windows/activitiescache.py +9 -4
- dissect/target/plugins/os/windows/adpolicy.py +2 -1
- dissect/target/plugins/os/windows/amcache.py +16 -13
- dissect/target/plugins/os/windows/defender.py +4 -3
- dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +3 -0
- dissect/target/plugins/os/windows/env.py +1 -2
- dissect/target/plugins/os/windows/exchange/exchange.py +6 -4
- dissect/target/plugins/os/windows/generic.py +68 -19
- dissect/target/plugins/os/windows/lnk.py +2 -0
- dissect/target/plugins/os/windows/locale.py +9 -3
- dissect/target/plugins/os/windows/log/etl.py +5 -4
- dissect/target/plugins/os/windows/log/evt.py +12 -8
- dissect/target/plugins/os/windows/log/evtx.py +9 -7
- dissect/target/plugins/os/windows/log/mssql.py +103 -0
- dissect/target/plugins/os/windows/log/pfro.py +2 -1
- dissect/target/plugins/os/windows/network.py +380 -0
- dissect/target/plugins/os/windows/notifications.py +6 -4
- dissect/target/plugins/os/windows/prefetch.py +7 -2
- dissect/target/plugins/os/windows/regf/7zip.py +9 -1
- dissect/target/plugins/os/windows/regf/applications.py +62 -0
- dissect/target/plugins/os/windows/regf/auditpol.py +2 -1
- dissect/target/plugins/os/windows/regf/bam.py +3 -1
- dissect/target/plugins/os/windows/regf/cit.py +14 -12
- dissect/target/plugins/os/windows/regf/clsid.py +6 -3
- dissect/target/plugins/os/windows/regf/firewall.py +2 -1
- dissect/target/plugins/os/windows/regf/mru.py +9 -8
- dissect/target/plugins/os/windows/regf/nethist.py +6 -3
- dissect/target/plugins/os/windows/regf/recentfilecache.py +3 -1
- dissect/target/plugins/os/windows/regf/regf.py +5 -1
- dissect/target/plugins/os/windows/regf/shellbags.py +351 -345
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
- dissect/target/plugins/os/windows/regf/usb.py +2 -1
- dissect/target/plugins/os/windows/regf/userassist.py +2 -1
- dissect/target/plugins/os/windows/registry.py +11 -0
- dissect/target/plugins/os/windows/services.py +3 -2
- dissect/target/plugins/os/windows/startupinfo.py +7 -2
- dissect/target/plugins/os/windows/syscache.py +5 -2
- dissect/target/plugins/os/windows/tasks.py +1 -1
- dissect/target/plugins/os/windows/thumbcache.py +11 -5
- dissect/target/plugins/os/windows/ual.py +12 -9
- dissect/target/plugins/os/windows/wer.py +21 -6
- dissect/target/plugins/os/windows/wua_history.py +0 -1
- dissect/target/target.py +13 -8
- dissect/target/tools/dump/utils.py +4 -0
- dissect/target/tools/fsutils.py +1 -1
- dissect/target/tools/info.py +1 -1
- dissect/target/tools/mount.py +15 -5
- dissect/target/tools/query.py +15 -9
- dissect/target/tools/shell.py +98 -9
- dissect/target/tools/utils.py +7 -7
- dissect/target/volume.py +4 -4
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/WHEEL +1 -1
- dissect/target/helpers/targetd.py +0 -58
- dissect/target/loaders/targetd.py +0 -223
- dissect/target/plugins/apps/texteditor/texteditor.py +0 -13
- dissect/target/plugins/os/unix/etc.py +0 -9
- /dissect/target/plugins/apps/{texteditor → database}/__init__.py +0 -0
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,17 @@
|
|
1
|
-
import
|
1
|
+
from __future__ import annotations
|
2
|
+
|
2
3
|
import ipaddress
|
3
4
|
import struct
|
4
5
|
from collections import namedtuple
|
5
6
|
from typing import Iterator
|
6
7
|
|
7
8
|
from dissect.cstruct import cstruct
|
8
|
-
from dissect.util.stream import BufferedStream
|
9
9
|
from dissect.util.ts import from_unix
|
10
10
|
|
11
11
|
from dissect.target.exceptions import UnsupportedPluginError
|
12
|
-
from dissect.target.helpers.fsutil import TargetPath
|
12
|
+
from dissect.target.helpers.fsutil import TargetPath, open_decompress
|
13
13
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
14
|
-
from dissect.target.plugin import
|
14
|
+
from dissect.target.plugin import Plugin, alias, export
|
15
15
|
from dissect.target.target import Target
|
16
16
|
|
17
17
|
UTMP_FIELDS = [
|
@@ -27,16 +27,12 @@ UTMP_FIELDS = [
|
|
27
27
|
|
28
28
|
BtmpRecord = TargetRecordDescriptor(
|
29
29
|
"linux/log/btmp",
|
30
|
-
|
31
|
-
*UTMP_FIELDS,
|
32
|
-
],
|
30
|
+
UTMP_FIELDS,
|
33
31
|
)
|
34
32
|
|
35
33
|
WtmpRecord = TargetRecordDescriptor(
|
36
34
|
"linux/log/wtmp",
|
37
|
-
|
38
|
-
*UTMP_FIELDS,
|
39
|
-
],
|
35
|
+
UTMP_FIELDS,
|
40
36
|
)
|
41
37
|
|
42
38
|
utmp_def = """
|
@@ -104,24 +100,13 @@ UTMP_ENTRY = namedtuple(
|
|
104
100
|
class UtmpFile:
|
105
101
|
"""utmp maintains a full accounting of the current status of the system"""
|
106
102
|
|
107
|
-
def __init__(self,
|
108
|
-
self.fh =
|
109
|
-
|
110
|
-
if "gz" in path:
|
111
|
-
self.compressed = True
|
112
|
-
else:
|
113
|
-
self.compressed = False
|
103
|
+
def __init__(self, path: TargetPath):
|
104
|
+
self.fh = open_decompress(path, "rb")
|
114
105
|
|
115
106
|
def __iter__(self):
|
116
|
-
if self.compressed:
|
117
|
-
gzip_entry = BufferedStream(gzip.open(self.fh, mode="rb"))
|
118
|
-
byte_stream = gzip_entry
|
119
|
-
else:
|
120
|
-
byte_stream = self.fh
|
121
|
-
|
122
107
|
while True:
|
123
108
|
try:
|
124
|
-
entry = c_utmp.entry(
|
109
|
+
entry = c_utmp.entry(self.fh)
|
125
110
|
|
126
111
|
r_type = ""
|
127
112
|
if entry.ut_type in c_utmp.Type:
|
@@ -151,7 +136,7 @@ class UtmpFile:
|
|
151
136
|
# ut_addr_v6 is parsed as IPv4 address. This could not lead to incorrect results.
|
152
137
|
ut_addr = ipaddress.ip_address(struct.pack("<i", entry.ut_addr_v6[0]))
|
153
138
|
|
154
|
-
|
139
|
+
yield UTMP_ENTRY(
|
155
140
|
ts=from_unix(entry.ut_tv.tv_sec),
|
156
141
|
ut_type=r_type,
|
157
142
|
ut_pid=entry.ut_pid,
|
@@ -162,25 +147,37 @@ class UtmpFile:
|
|
162
147
|
ut_addr=ut_addr,
|
163
148
|
)
|
164
149
|
|
165
|
-
yield utmp_entry
|
166
150
|
except EOFError:
|
167
151
|
break
|
168
152
|
|
169
153
|
|
170
154
|
class UtmpPlugin(Plugin):
|
171
|
-
|
172
|
-
|
155
|
+
"""Unix utmp log plugin."""
|
156
|
+
|
157
|
+
def __init__(self, target: Target):
|
158
|
+
super().__init__(target)
|
159
|
+
self.btmp_paths = list(self.target.fs.path("/").glob("var/log/btmp*"))
|
160
|
+
self.wtmp_paths = list(self.target.fs.path("/").glob("var/log/wtmp*"))
|
161
|
+
self.utmp_paths = list(self.target.fs.path("/").glob("var/run/utmp*"))
|
173
162
|
|
174
163
|
def check_compatible(self) -> None:
|
175
|
-
if not self.
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
164
|
+
if not any(self.btmp_paths + self.wtmp_paths + self.utmp_paths):
|
165
|
+
raise UnsupportedPluginError("No wtmp and/or btmp log files found")
|
166
|
+
|
167
|
+
def _build_record(self, record: TargetRecordDescriptor, entry: UTMP_ENTRY) -> Iterator[BtmpRecord | WtmpRecord]:
|
168
|
+
return record(
|
169
|
+
ts=entry.ts,
|
170
|
+
ut_type=entry.ut_type,
|
171
|
+
ut_pid=entry.ut_pid,
|
172
|
+
ut_user=entry.ut_user,
|
173
|
+
ut_line=entry.ut_line,
|
174
|
+
ut_id=entry.ut_id,
|
175
|
+
ut_host=entry.ut_host,
|
176
|
+
ut_addr=entry.ut_addr,
|
177
|
+
_target=self.target,
|
178
|
+
)
|
179
|
+
|
180
|
+
@export(record=BtmpRecord)
|
184
181
|
def btmp(self) -> Iterator[BtmpRecord]:
|
185
182
|
"""Return failed login attempts stored in the btmp file.
|
186
183
|
|
@@ -190,26 +187,18 @@ class UtmpPlugin(Plugin):
|
|
190
187
|
- https://en.wikipedia.org/wiki/Utmp
|
191
188
|
- https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
|
192
189
|
"""
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
for entry in btmp:
|
198
|
-
yield BtmpRecord(
|
199
|
-
ts=entry.ts,
|
200
|
-
ut_type=entry.ut_type,
|
201
|
-
ut_pid=entry.ut_pid,
|
202
|
-
ut_user=entry.ut_user,
|
203
|
-
ut_line=entry.ut_line,
|
204
|
-
ut_id=entry.ut_id,
|
205
|
-
ut_host=entry.ut_host,
|
206
|
-
ut_addr=entry.ut_addr,
|
207
|
-
_target=self.target,
|
208
|
-
)
|
190
|
+
for path in self.btmp_paths:
|
191
|
+
if not path.is_file():
|
192
|
+
self.target.log.warning("Unable to parse btmp file: %s is not a file", path)
|
193
|
+
continue
|
209
194
|
|
210
|
-
|
195
|
+
for entry in UtmpFile(path):
|
196
|
+
yield self._build_record(BtmpRecord, entry)
|
197
|
+
|
198
|
+
@alias("utmp")
|
199
|
+
@export(record=WtmpRecord)
|
211
200
|
def wtmp(self) -> Iterator[WtmpRecord]:
|
212
|
-
"""
|
201
|
+
"""Yield contents of wtmp log files.
|
213
202
|
|
214
203
|
The wtmp file contains the historical data of the utmp file. The utmp file contains information about users
|
215
204
|
logins at which terminals, logouts, system events and current status of the system, system boot time
|
@@ -218,19 +207,11 @@ class UtmpPlugin(Plugin):
|
|
218
207
|
References:
|
219
208
|
- https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
|
220
209
|
"""
|
221
|
-
|
222
|
-
for
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
ut_pid=entry.ut_pid,
|
230
|
-
ut_user=entry.ut_user,
|
231
|
-
ut_line=entry.ut_line,
|
232
|
-
ut_id=entry.ut_id,
|
233
|
-
ut_host=entry.ut_host,
|
234
|
-
ut_addr=entry.ut_addr,
|
235
|
-
_target=self.target,
|
236
|
-
)
|
210
|
+
|
211
|
+
for path in self.wtmp_paths + self.utmp_paths:
|
212
|
+
if not path.is_file():
|
213
|
+
self.target.log.warning("Unable to parse wtmp file: %s is not a file", path)
|
214
|
+
continue
|
215
|
+
|
216
|
+
for entry in UtmpFile(path):
|
217
|
+
yield self._build_record(WtmpRecord, entry)
|
@@ -1,15 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from enum import Enum
|
4
|
-
from typing import Iterator
|
5
4
|
|
6
|
-
from dissect.target import Target
|
7
|
-
from dissect.target.exceptions import UnsupportedPluginError
|
8
5
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
9
|
-
from dissect.target.plugin import
|
6
|
+
from dissect.target.plugin import NamespacePlugin
|
10
7
|
|
11
8
|
PackageManagerLogRecord = TargetRecordDescriptor(
|
12
|
-
"unix/log
|
9
|
+
"unix/packagemanager/log",
|
13
10
|
[
|
14
11
|
("datetime", "ts"),
|
15
12
|
("string", "package_manager"),
|
@@ -22,6 +19,8 @@ PackageManagerLogRecord = TargetRecordDescriptor(
|
|
22
19
|
|
23
20
|
|
24
21
|
class OperationTypes(Enum):
|
22
|
+
"""Valid operation types."""
|
23
|
+
|
25
24
|
Install = "install"
|
26
25
|
Update = "update"
|
27
26
|
Downgrade = "downgrade"
|
@@ -45,37 +44,5 @@ class OperationTypes(Enum):
|
|
45
44
|
return OperationTypes.Other
|
46
45
|
|
47
46
|
|
48
|
-
class PackageManagerPlugin(
|
47
|
+
class PackageManagerPlugin(NamespacePlugin):
|
49
48
|
__namespace__ = "packagemanager"
|
50
|
-
__findable__ = False
|
51
|
-
|
52
|
-
TOOLS = [
|
53
|
-
"apt",
|
54
|
-
"yum",
|
55
|
-
"zypper",
|
56
|
-
]
|
57
|
-
|
58
|
-
def __init__(self, target: Target):
|
59
|
-
super().__init__(target)
|
60
|
-
self._plugins = []
|
61
|
-
for entry in self.TOOLS:
|
62
|
-
try:
|
63
|
-
self._plugins.append(getattr(self.target, entry))
|
64
|
-
except Exception:
|
65
|
-
target.log.exception(f"Failed to load tool plugin: {entry}")
|
66
|
-
|
67
|
-
def check_compatible(self) -> None:
|
68
|
-
if not len(self._plugins):
|
69
|
-
raise UnsupportedPluginError("No compatible plugins found")
|
70
|
-
|
71
|
-
def _func(self, f: str) -> Iterator[PackageManagerLogRecord]:
|
72
|
-
for p in self._plugins:
|
73
|
-
try:
|
74
|
-
yield from getattr(p, f)()
|
75
|
-
except Exception:
|
76
|
-
self.target.log.exception("Failed to execute package manager plugin: %s.%s", p._name, f)
|
77
|
-
|
78
|
-
@export(record=PackageManagerLogRecord)
|
79
|
-
def logs(self) -> Iterator[PackageManagerLogRecord]:
|
80
|
-
"""Returns logs from all available Unix package managers."""
|
81
|
-
yield from self._func("logs")
|
@@ -25,6 +25,8 @@ UnixShadowRecord = TargetRecordDescriptor(
|
|
25
25
|
|
26
26
|
|
27
27
|
class ShadowPlugin(Plugin):
|
28
|
+
"""Unix shadow passwords plugin."""
|
29
|
+
|
28
30
|
def check_compatible(self) -> None:
|
29
31
|
if not self.target.fs.path("/etc/shadow").exists():
|
30
32
|
raise UnsupportedPluginError("No shadow file found")
|
@@ -36,7 +38,7 @@ class ShadowPlugin(Plugin):
|
|
36
38
|
"""Yield shadow records from /etc/shadow files.
|
37
39
|
|
38
40
|
Resources:
|
39
|
-
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
|
41
|
+
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
|
40
42
|
"""
|
41
43
|
|
42
44
|
seen_hashes = set()
|
@@ -0,0 +1,132 @@
|
|
1
|
+
from typing import Iterator
|
2
|
+
|
3
|
+
from dissect.target.exceptions import UnsupportedPluginError
|
4
|
+
from dissect.target.helpers import configutil
|
5
|
+
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
6
|
+
from dissect.target.helpers.fsutil import TargetPath
|
7
|
+
from dissect.target.helpers.record import create_extended_descriptor
|
8
|
+
from dissect.target.plugin import Plugin, alias, export
|
9
|
+
from dissect.target.plugins.general.users import UserDetails
|
10
|
+
from dissect.target.target import Target
|
11
|
+
|
12
|
+
TrashRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
13
|
+
"linux/filesystem/recyclebin",
|
14
|
+
[
|
15
|
+
("datetime", "ts"),
|
16
|
+
("path", "path"),
|
17
|
+
("filesize", "filesize"),
|
18
|
+
("path", "deleted_path"),
|
19
|
+
("path", "source"),
|
20
|
+
],
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
class GnomeTrashPlugin(Plugin):
|
25
|
+
"""Linux GNOME Trash plugin."""
|
26
|
+
|
27
|
+
PATHS = [
|
28
|
+
# Default $XDG_DATA_HOME/Trash
|
29
|
+
".local/share/Trash",
|
30
|
+
]
|
31
|
+
|
32
|
+
def __init__(self, target: Target):
|
33
|
+
super().__init__(target)
|
34
|
+
self.trashes = list(self._garbage_collector())
|
35
|
+
|
36
|
+
def _garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]:
|
37
|
+
"""it aint much, but its honest work"""
|
38
|
+
for user_details in self.target.user_details.all_with_home():
|
39
|
+
for trash_path in self.PATHS:
|
40
|
+
if (path := user_details.home_path.joinpath(trash_path)).exists():
|
41
|
+
yield user_details, path
|
42
|
+
|
43
|
+
def check_compatible(self) -> None:
|
44
|
+
if not self.trashes:
|
45
|
+
raise UnsupportedPluginError("No Trash folder(s) found")
|
46
|
+
|
47
|
+
@export(record=TrashRecord)
|
48
|
+
@alias(name="recyclebin")
|
49
|
+
def trash(self) -> Iterator[TrashRecord]:
|
50
|
+
"""Yield deleted files from GNOME Trash folders.
|
51
|
+
|
52
|
+
Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``.
|
53
|
+
Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop.
|
54
|
+
|
55
|
+
Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``.
|
56
|
+
|
57
|
+
Resources:
|
58
|
+
- https://specifications.freedesktop.org/trash-spec/latest/
|
59
|
+
- https://github.com/GNOME/glib/blob/main/gio/glocalfile.c
|
60
|
+
- https://specifications.freedesktop.org/basedir-spec/latest/
|
61
|
+
|
62
|
+
Yields ``TrashRecord`` records with the following fields:
|
63
|
+
|
64
|
+
.. code-block:: text
|
65
|
+
|
66
|
+
ts (datetime): timestamp when the file was deleted or for expunged files when it could not be permanently deleted
|
67
|
+
path (path): path where the file was located before it was deleted
|
68
|
+
filesize (filesize): size in bytes of the deleted file
|
69
|
+
deleted_path (path): path to the current location of the deleted file
|
70
|
+
source (path): path to the .trashinfo file
|
71
|
+
""" # noqa: E501
|
72
|
+
|
73
|
+
for user_details, trash in self.trashes:
|
74
|
+
for trash_info_file in trash.glob("info/*.trashinfo"):
|
75
|
+
trash_info = configutil.parse(trash_info_file, hint="ini").get("Trash Info", {})
|
76
|
+
original_path = self.target.fs.path(trash_info.get("Path", ""))
|
77
|
+
|
78
|
+
# We use the basename of the .trashinfo file and not the Path variable inside the
|
79
|
+
# ini file. This way we can keep duplicate basenames of trashed files separated correctly.
|
80
|
+
deleted_path = trash / "files" / trash_info_file.name.replace(".trashinfo", "")
|
81
|
+
|
82
|
+
if deleted_path.exists():
|
83
|
+
deleted_files = [deleted_path]
|
84
|
+
|
85
|
+
if deleted_path.is_dir():
|
86
|
+
for child in deleted_path.rglob("*"):
|
87
|
+
deleted_files.append(child)
|
88
|
+
|
89
|
+
for file in deleted_files:
|
90
|
+
# NOTE: We currently do not 'fix' the original_path of files inside deleted directories.
|
91
|
+
# This would require guessing where the parent folder starts, which is impossible without
|
92
|
+
# making assumptions.
|
93
|
+
yield TrashRecord(
|
94
|
+
ts=trash_info.get("DeletionDate", 0),
|
95
|
+
path=original_path,
|
96
|
+
filesize=file.lstat().st_size if file.is_file() else None,
|
97
|
+
deleted_path=file,
|
98
|
+
source=trash_info_file,
|
99
|
+
_user=user_details.user,
|
100
|
+
_target=self.target,
|
101
|
+
)
|
102
|
+
|
103
|
+
# We cannot determine if the deleted entry is a directory since the path does
|
104
|
+
# not exist at $TRASH/files, so we work with what we have instead.
|
105
|
+
else:
|
106
|
+
self.target.log.warning(f"Expected trashed file(s) at {deleted_path}")
|
107
|
+
yield TrashRecord(
|
108
|
+
ts=trash_info.get("DeletionDate", 0),
|
109
|
+
path=original_path,
|
110
|
+
filesize=0,
|
111
|
+
deleted_path=deleted_path,
|
112
|
+
source=trash_info_file,
|
113
|
+
_user=user_details.user,
|
114
|
+
_target=self.target,
|
115
|
+
)
|
116
|
+
|
117
|
+
# We also iterate expunged folders, they can contain files that could not be
|
118
|
+
# deleted when the user pressed the "empty trash" button in the file manager.
|
119
|
+
# Resources:
|
120
|
+
# - https://gitlab.gnome.org/GNOME/glib/-/issues/1665
|
121
|
+
# - https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012
|
122
|
+
for item in (trash / "expunged").rglob("*/*"):
|
123
|
+
stat = item.lstat()
|
124
|
+
yield TrashRecord(
|
125
|
+
ts=stat.st_mtime, # NOTE: This is the timestamp at which the file failed to delete
|
126
|
+
path=None, # We do not know the original file path
|
127
|
+
filesize=stat.st_size if item.is_file() else None,
|
128
|
+
deleted_path=item,
|
129
|
+
source=trash / "expunged",
|
130
|
+
_user=user_details.user,
|
131
|
+
_target=self.target,
|
132
|
+
)
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import operator
|
4
4
|
import struct
|
5
|
-
from typing import Any, Iterator
|
5
|
+
from typing import Any, Iterator
|
6
|
+
|
7
|
+
from flow.record.fieldtypes import windows_path
|
6
8
|
|
7
9
|
from dissect.target.exceptions import RegistryError, RegistryValueNotFoundError
|
8
10
|
from dissect.target.filesystem import Filesystem
|
@@ -10,6 +12,14 @@ from dissect.target.helpers.record import WindowsUserRecord
|
|
10
12
|
from dissect.target.plugin import OperatingSystem, OSPlugin, export
|
11
13
|
from dissect.target.target import Target
|
12
14
|
|
15
|
+
ARCH_MAP = {
|
16
|
+
"x86": 32,
|
17
|
+
"IA64": 64,
|
18
|
+
"ARM64": 64,
|
19
|
+
"EM64T": 64,
|
20
|
+
"AMD64": 64,
|
21
|
+
}
|
22
|
+
|
13
23
|
|
14
24
|
class WindowsPlugin(OSPlugin):
|
15
25
|
CURRENT_VERSION_KEY = "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion"
|
@@ -26,7 +36,7 @@ class WindowsPlugin(OSPlugin):
|
|
26
36
|
)
|
27
37
|
|
28
38
|
@classmethod
|
29
|
-
def detect(cls, target: Target) ->
|
39
|
+
def detect(cls, target: Target) -> Filesystem | None:
|
30
40
|
for fs in target.filesystems:
|
31
41
|
if fs.exists("/windows/system32") or fs.exists("/winnt"):
|
32
42
|
return fs
|
@@ -90,7 +100,7 @@ class WindowsPlugin(OSPlugin):
|
|
90
100
|
self.target.log.warning("Unknown drive letter for sysvol")
|
91
101
|
|
92
102
|
@export(property=True)
|
93
|
-
def hostname(self) ->
|
103
|
+
def hostname(self) -> str | None:
|
94
104
|
key = "HKLM\\SYSTEM\\ControlSet001\\Control\\Computername\\Computername"
|
95
105
|
try:
|
96
106
|
return self.target.registry.value(key, "Computername").value
|
@@ -99,28 +109,7 @@ class WindowsPlugin(OSPlugin):
|
|
99
109
|
|
100
110
|
@export(property=True)
|
101
111
|
def ips(self) -> list[str]:
|
102
|
-
|
103
|
-
fields = ["IPAddress", "DhcpIPAddress"]
|
104
|
-
ips = set()
|
105
|
-
|
106
|
-
for r in self.target.registry.keys(key):
|
107
|
-
for s in r.subkeys():
|
108
|
-
for field in fields:
|
109
|
-
try:
|
110
|
-
ip = s.value(field).value
|
111
|
-
except RegistryValueNotFoundError:
|
112
|
-
continue
|
113
|
-
|
114
|
-
if isinstance(ip, str):
|
115
|
-
ip = [ip]
|
116
|
-
|
117
|
-
for i in ip:
|
118
|
-
if i == "0.0.0.0":
|
119
|
-
continue
|
120
|
-
|
121
|
-
ips.add(i)
|
122
|
-
|
123
|
-
return list(ips)
|
112
|
+
return list(set(map(str, self.target.network.ips())))
|
124
113
|
|
125
114
|
def _get_version_reg_value(self, value_name: str) -> Any:
|
126
115
|
try:
|
@@ -130,7 +119,7 @@ class WindowsPlugin(OSPlugin):
|
|
130
119
|
|
131
120
|
return value
|
132
121
|
|
133
|
-
def _legacy_current_version(self) ->
|
122
|
+
def _legacy_current_version(self) -> str | None:
|
134
123
|
"""Returns the NT version as used up to and including NT 6.3.
|
135
124
|
|
136
125
|
This corresponds with Windows 8 / Windows 2012 Server.
|
@@ -142,7 +131,7 @@ class WindowsPlugin(OSPlugin):
|
|
142
131
|
"""
|
143
132
|
return self._get_version_reg_value("CurrentVersion")
|
144
133
|
|
145
|
-
def _major_version(self) ->
|
134
|
+
def _major_version(self) -> int | None:
|
146
135
|
"""Return the NT major version number (starting from NT 10.0 / Windows 10).
|
147
136
|
|
148
137
|
Returns:
|
@@ -152,7 +141,7 @@ class WindowsPlugin(OSPlugin):
|
|
152
141
|
"""
|
153
142
|
return self._get_version_reg_value("CurrentMajorVersionNumber")
|
154
143
|
|
155
|
-
def _minor_version(self) ->
|
144
|
+
def _minor_version(self) -> int | None:
|
156
145
|
"""Return the NT minor version number (starting from NT 10.0 / Windows 10).
|
157
146
|
|
158
147
|
Returns:
|
@@ -162,7 +151,7 @@ class WindowsPlugin(OSPlugin):
|
|
162
151
|
"""
|
163
152
|
return self._get_version_reg_value("CurrentMinorVersionNumber")
|
164
153
|
|
165
|
-
def _nt_version(self) ->
|
154
|
+
def _nt_version(self) -> int | None:
|
166
155
|
"""Return the Windows NT version in x.y format.
|
167
156
|
|
168
157
|
For systems up to and including NT 6.3 (Win 8 / Win 2012 Server) this
|
@@ -190,7 +179,7 @@ class WindowsPlugin(OSPlugin):
|
|
190
179
|
return version
|
191
180
|
|
192
181
|
@export(property=True)
|
193
|
-
def version(self) ->
|
182
|
+
def version(self) -> str | None:
|
194
183
|
"""Return a string representation of the Windows version of the target.
|
195
184
|
|
196
185
|
For Windows versions before Windows 10 this looks like::
|
@@ -276,7 +265,7 @@ class WindowsPlugin(OSPlugin):
|
|
276
265
|
return version_string
|
277
266
|
|
278
267
|
@export(property=True)
|
279
|
-
def architecture(self) ->
|
268
|
+
def architecture(self) -> str | None:
|
280
269
|
"""
|
281
270
|
Returns a dict containing the architecture and bitness of the system
|
282
271
|
|
@@ -284,19 +273,11 @@ class WindowsPlugin(OSPlugin):
|
|
284
273
|
Dict: arch: architecture, bitness: bits
|
285
274
|
"""
|
286
275
|
|
287
|
-
arch_strings = {
|
288
|
-
"x86": 32,
|
289
|
-
"IA64": 64,
|
290
|
-
"ARM64": 64,
|
291
|
-
"EM64T": 64,
|
292
|
-
"AMD64": 64,
|
293
|
-
}
|
294
|
-
|
295
276
|
key = "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
|
296
277
|
|
297
278
|
try:
|
298
279
|
arch = self.target.registry.key(key).value("PROCESSOR_ARCHITECTURE").value
|
299
|
-
bits =
|
280
|
+
bits = ARCH_MAP.get(arch)
|
300
281
|
|
301
282
|
# return {"arch": arch, "bitness": bits}
|
302
283
|
if bits == 64:
|
@@ -333,7 +314,7 @@ class WindowsPlugin(OSPlugin):
|
|
333
314
|
yield WindowsUserRecord(
|
334
315
|
sid=subkey.name,
|
335
316
|
name=name,
|
336
|
-
home=home,
|
317
|
+
home=windows_path(home),
|
337
318
|
_target=self.target,
|
338
319
|
)
|
339
320
|
|
@@ -1,3 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Iterator
|
5
|
+
|
1
6
|
from dissect.sql import sqlite3
|
2
7
|
from dissect.util.ts import from_unix
|
3
8
|
|
@@ -42,8 +47,8 @@ class ActivitiesCachePlugin(Plugin):
|
|
42
47
|
"""Plugin that parses the ActivitiesCache.db on newer Windows 10 machines.
|
43
48
|
|
44
49
|
References:
|
45
|
-
https://www.cclsolutionsgroup.com/resources/technical-papers
|
46
|
-
https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
|
50
|
+
- https://www.cclsolutionsgroup.com/resources/technical-papers
|
51
|
+
- https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
|
47
52
|
"""
|
48
53
|
|
49
54
|
def __init__(self, target):
|
@@ -62,7 +67,7 @@ class ActivitiesCachePlugin(Plugin):
|
|
62
67
|
raise UnsupportedPluginError("No ActiviesCache.db files found")
|
63
68
|
|
64
69
|
@export(record=ActivitiesCacheRecord)
|
65
|
-
def activitiescache(self):
|
70
|
+
def activitiescache(self) -> Iterator[ActivitiesCacheRecord]:
|
66
71
|
"""Return ActivitiesCache.db database content.
|
67
72
|
|
68
73
|
The Windows Activities Cache database keeps track of activity on a device, such as application and services
|
@@ -143,7 +148,7 @@ class ActivitiesCachePlugin(Plugin):
|
|
143
148
|
)
|
144
149
|
|
145
150
|
|
146
|
-
def mkts(ts):
|
151
|
+
def mkts(ts: int) -> datetime | None:
|
147
152
|
"""Timestamps inside ActivitiesCache.db are stored in a Unix-like format.
|
148
153
|
|
149
154
|
Source: https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from struct import unpack
|
2
|
+
from typing import Iterator
|
2
3
|
|
3
4
|
from defusedxml import ElementTree
|
4
5
|
from dissect.cstruct import cstruct
|
@@ -90,7 +91,7 @@ class ADPolicyPlugin(Plugin):
|
|
90
91
|
self.target.log.warning("Unable to read XML policy file: %s", error)
|
91
92
|
|
92
93
|
@export(record=ADPolicyRecord)
|
93
|
-
def adpolicy(self):
|
94
|
+
def adpolicy(self) -> Iterator[ADPolicyRecord]:
|
94
95
|
"""Return all AD policies (also known as GPOs or Group Policy Objects).
|
95
96
|
|
96
97
|
An Active Directory (AD) maintains global policies that should be adhered by all systems in the domain.
|