dissect.target 3.18.dev15__py3-none-any.whl → 3.19__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/filesystem.py +44 -25
- dissect/target/filesystems/config.py +32 -21
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/filesystems/itunes.py +1 -1
- dissect/target/filesystems/tar.py +1 -1
- dissect/target/filesystems/zip.py +81 -46
- dissect/target/helpers/config.py +22 -7
- dissect/target/helpers/configutil.py +69 -5
- dissect/target/helpers/cyber.py +4 -2
- dissect/target/helpers/fsutil.py +32 -4
- dissect/target/helpers/loaderutil.py +26 -7
- dissect/target/helpers/network_managers.py +22 -7
- dissect/target/helpers/record.py +37 -0
- dissect/target/helpers/record_modifier.py +23 -4
- dissect/target/helpers/shell_application_ids.py +732 -0
- dissect/target/helpers/utils.py +11 -0
- dissect/target/loader.py +1 -0
- dissect/target/loaders/ab.py +285 -0
- dissect/target/loaders/libvirt.py +40 -0
- dissect/target/loaders/mqtt.py +14 -1
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/utm.py +3 -0
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +60 -3
- dissect/target/plugins/apps/browser/chrome.py +1 -0
- dissect/target/plugins/apps/browser/chromium.py +7 -5
- dissect/target/plugins/apps/browser/edge.py +1 -0
- dissect/target/plugins/apps/browser/firefox.py +82 -36
- dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
- dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
- dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
- dissect/target/plugins/apps/ssh/openssh.py +1 -1
- dissect/target/plugins/apps/ssh/ssh.py +177 -0
- dissect/target/plugins/apps/texteditor/__init__.py +0 -0
- dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
- dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
- dissect/target/plugins/child/qemu.py +21 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
- dissect/target/plugins/filesystem/unix/capability.py +102 -87
- dissect/target/plugins/filesystem/walkfs.py +32 -21
- dissect/target/plugins/filesystem/yara.py +144 -23
- dissect/target/plugins/general/network.py +82 -0
- dissect/target/plugins/general/users.py +14 -10
- dissect/target/plugins/os/unix/_os.py +19 -5
- dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
- dissect/target/plugins/os/unix/esxi/_os.py +29 -23
- dissect/target/plugins/os/unix/etc/etc.py +5 -8
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/plugins/os/unix/linux/_os.py +15 -14
- dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
- dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
- dissect/target/plugins/os/unix/locale.py +17 -6
- dissect/target/plugins/os/unix/shadow.py +47 -31
- dissect/target/plugins/os/windows/_os.py +4 -4
- dissect/target/plugins/os/windows/adpolicy.py +4 -1
- dissect/target/plugins/os/windows/catroot.py +1 -11
- dissect/target/plugins/os/windows/credential/__init__.py +0 -0
- dissect/target/plugins/os/windows/credential/lsa.py +174 -0
- dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
- dissect/target/plugins/os/windows/defender.py +6 -3
- dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
- dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
- dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
- dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
- dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
- dissect/target/plugins/os/windows/jumplist.py +292 -0
- dissect/target/plugins/os/windows/lnk.py +96 -93
- dissect/target/plugins/os/windows/regf/shellbags.py +8 -5
- dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
- dissect/target/plugins/os/windows/regf/usb.py +179 -114
- dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
- dissect/target/plugins/os/windows/wua_history.py +1073 -0
- dissect/target/target.py +4 -3
- dissect/target/tools/fs.py +53 -15
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +11 -4
- dissect/target/tools/query.py +2 -2
- dissect/target/tools/shell.py +505 -333
- dissect/target/tools/utils.py +23 -2
- dissect/target/tools/yara.py +65 -0
- dissect/target/volumes/md.py +2 -2
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
- dissect/target/helpers/ssh.py +0 -177
- /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
- {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
dissect/target/filesystem.py
CHANGED
@@ -228,10 +228,9 @@ class Filesystem:
|
|
228
228
|
topdown: bool = True,
|
229
229
|
onerror: Optional[Callable] = None,
|
230
230
|
followlinks: bool = False,
|
231
|
-
) -> Iterator[str]:
|
232
|
-
"""
|
233
|
-
|
234
|
-
It walks across all the files inside ``path`` recursively.
|
231
|
+
) -> Iterator[tuple[str, list[str], list[str]]]:
|
232
|
+
"""Recursively walk a directory pointed to by ``path``, returning the string representation of both files
|
233
|
+
and directories.
|
235
234
|
|
236
235
|
Args:
|
237
236
|
path: The path to walk on the filesystem.
|
@@ -250,10 +249,9 @@ class Filesystem:
|
|
250
249
|
topdown: bool = True,
|
251
250
|
onerror: Optional[Callable] = None,
|
252
251
|
followlinks: bool = False,
|
253
|
-
) -> Iterator[FilesystemEntry]:
|
254
|
-
"""
|
255
|
-
|
256
|
-
It walks across all the files inside ``path`` recursively.
|
252
|
+
) -> Iterator[tuple[list[FilesystemEntry], list[FilesystemEntry], list[FilesystemEntry]]]:
|
253
|
+
"""Recursively walk a directory pointed to by ``path``, returning :class:`FilesystemEntry` of files
|
254
|
+
and directories.
|
257
255
|
|
258
256
|
Args:
|
259
257
|
path: The path to walk on the filesystem.
|
@@ -266,6 +264,19 @@ class Filesystem:
|
|
266
264
|
"""
|
267
265
|
return self.get(path).walk_ext(topdown, onerror, followlinks)
|
268
266
|
|
267
|
+
def recurse(self, path: str) -> Iterator[FilesystemEntry]:
|
268
|
+
"""Recursively walk a directory and yield contents as :class:`FilesystemEntry`.
|
269
|
+
|
270
|
+
Does not follow symbolic links.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
path: The path to recursively walk on the target filesystem.
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
An iterator of :class:`FilesystemEntry`.
|
277
|
+
"""
|
278
|
+
return self.get(path).recurse()
|
279
|
+
|
269
280
|
def glob(self, pattern: str) -> Iterator[str]:
|
270
281
|
"""Iterate over the directory part of ``pattern``, returning entries matching ``pattern`` as strings.
|
271
282
|
|
@@ -578,10 +589,9 @@ class FilesystemEntry:
|
|
578
589
|
topdown: bool = True,
|
579
590
|
onerror: Optional[Callable] = None,
|
580
591
|
followlinks: bool = False,
|
581
|
-
) -> Iterator[str]:
|
582
|
-
"""
|
583
|
-
|
584
|
-
It walks across all the files inside the entry recursively.
|
592
|
+
) -> Iterator[tuple[str, list[str], list[str]]]:
|
593
|
+
"""Recursively walk a directory and yield its contents as strings split in a tuple
|
594
|
+
of lists of files, directories and symlinks.
|
585
595
|
|
586
596
|
These contents include::
|
587
597
|
- files
|
@@ -603,15 +613,9 @@ class FilesystemEntry:
|
|
603
613
|
topdown: bool = True,
|
604
614
|
onerror: Optional[Callable] = None,
|
605
615
|
followlinks: bool = False,
|
606
|
-
) -> Iterator[FilesystemEntry]:
|
607
|
-
"""
|
608
|
-
|
609
|
-
It walks across all the files inside the entry recursively.
|
610
|
-
|
611
|
-
These contents include::
|
612
|
-
- files
|
613
|
-
- directories
|
614
|
-
- symboliclinks
|
616
|
+
) -> Iterator[tuple[list[FilesystemEntry], list[FilesystemEntry], list[FilesystemEntry]]]:
|
617
|
+
"""Recursively walk a directory and yield its contents as :class:`FilesystemEntry` split in a tuple of
|
618
|
+
lists of files, directories and symlinks.
|
615
619
|
|
616
620
|
Args:
|
617
621
|
topdown: ``True`` puts this entry at the top of the list, ``False`` puts this entry at the bottom.
|
@@ -619,10 +623,20 @@ class FilesystemEntry:
|
|
619
623
|
followlinks: ``True`` if we want to follow any symbolic link
|
620
624
|
|
621
625
|
Returns:
|
622
|
-
An iterator of :class:`FilesystemEntry`.
|
626
|
+
An iterator of tuples :class:`FilesystemEntry`.
|
623
627
|
"""
|
624
628
|
yield from fsutil.walk_ext(self, topdown, onerror, followlinks)
|
625
629
|
|
630
|
+
def recurse(self) -> Iterator[FilesystemEntry]:
|
631
|
+
"""Recursively walk a directory and yield its contents as :class:`FilesystemEntry`.
|
632
|
+
|
633
|
+
Does not follow symbolic links.
|
634
|
+
|
635
|
+
Returns:
|
636
|
+
An iterator of :class:`FilesystemEntry`.
|
637
|
+
"""
|
638
|
+
yield from fsutil.recurse(self)
|
639
|
+
|
626
640
|
def glob(self, pattern: str) -> Iterator[str]:
|
627
641
|
"""Iterate over this directory part of ``patern``, returning entries matching ``pattern`` as strings.
|
628
642
|
|
@@ -739,7 +753,7 @@ class FilesystemEntry:
|
|
739
753
|
"""
|
740
754
|
log.debug("%r::readlink_ext()", self)
|
741
755
|
# Default behavior, resolve link own filesystem.
|
742
|
-
return fsutil.resolve_link(
|
756
|
+
return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator)
|
743
757
|
|
744
758
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
745
759
|
"""Determine the stat information of this entry.
|
@@ -1453,10 +1467,15 @@ class LayerFilesystem(Filesystem):
|
|
1453
1467
|
"""Get a :class:`FilesystemEntry` relative to a specific entry."""
|
1454
1468
|
parts = path.split("/")
|
1455
1469
|
|
1456
|
-
for part in parts:
|
1470
|
+
for i, part in enumerate(parts):
|
1457
1471
|
if entry.is_symlink():
|
1458
1472
|
# Resolve using the RootFilesystem instead of the entry's Filesystem
|
1459
|
-
entry = fsutil.resolve_link(
|
1473
|
+
entry = fsutil.resolve_link(
|
1474
|
+
self,
|
1475
|
+
entry.readlink(),
|
1476
|
+
"/".join(parts[:i]),
|
1477
|
+
alt_separator=entry.fs.alt_separator,
|
1478
|
+
)
|
1460
1479
|
entry = entry.get(part)
|
1461
1480
|
|
1462
1481
|
return entry
|
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
3
3
|
import io
|
4
4
|
import textwrap
|
5
5
|
from logging import getLogger
|
6
|
-
from typing import Any, BinaryIO, Iterator, Optional
|
6
|
+
from typing import Any, BinaryIO, Iterator, Optional
|
7
7
|
|
8
8
|
from dissect.target import Target
|
9
9
|
from dissect.target.exceptions import ConfigurationParsingError, FileNotFoundError
|
10
|
-
from dissect.target.filesystem import
|
10
|
+
from dissect.target.filesystem import FilesystemEntry, VirtualFilesystem
|
11
11
|
from dissect.target.helpers import fsutil
|
12
12
|
from dissect.target.helpers.configutil import ConfigurationParser, parse
|
13
13
|
|
@@ -46,7 +46,7 @@ class ConfigurationFilesystem(VirtualFilesystem):
|
|
46
46
|
super().__init__(**kwargs)
|
47
47
|
self.root.top = target.fs.get(path)
|
48
48
|
|
49
|
-
def _get_till_file(self, path: str, relentry: FilesystemEntry) -> tuple[list[str], FilesystemEntry]:
|
49
|
+
def _get_till_file(self, path: str, relentry: FilesystemEntry | None) -> tuple[list[str], FilesystemEntry]:
|
50
50
|
"""Searches for the file entry that is pointed to by ``path``.
|
51
51
|
|
52
52
|
The ``path`` could contain ``key`` entries too, so it searches for the entry from
|
@@ -56,9 +56,13 @@ class ConfigurationFilesystem(VirtualFilesystem):
|
|
56
56
|
A list of ``parts`` containing keys: [keys, into, the, file].
|
57
57
|
And the resolved entry: Entry(filename)
|
58
58
|
"""
|
59
|
+
|
59
60
|
entry = relentry or self.root
|
61
|
+
root_path = relentry.path if relentry else self.root.top.path
|
60
62
|
|
61
|
-
|
63
|
+
# Calculate the relative path
|
64
|
+
relpath = fsutil.relpath(path, root_path, alt_separator=self.alt_separator)
|
65
|
+
path = fsutil.normalize(relpath, alt_separator=self.alt_separator).strip("/")
|
62
66
|
|
63
67
|
if not path:
|
64
68
|
return [], entry
|
@@ -85,10 +89,8 @@ class ConfigurationFilesystem(VirtualFilesystem):
|
|
85
89
|
|
86
90
|
return parts[idx:], entry
|
87
91
|
|
88
|
-
def get(
|
89
|
-
|
90
|
-
) -> Union[FilesystemEntry, ConfigurationEntry]:
|
91
|
-
"""Retrieve a :class:`ConfigurationEntry` or :class:`.FilesystemEntry` relative to the root or ``relentry``.
|
92
|
+
def get(self, path: str, relentry: Optional[FilesystemEntry] = None, *args, **kwargs) -> ConfigurationEntry:
|
93
|
+
"""Retrieve a :class:`ConfigurationEntry` relative to the root or ``relentry``.
|
92
94
|
|
93
95
|
Raises:
|
94
96
|
FileNotFoundError: if it could not find the entry.
|
@@ -96,7 +98,7 @@ class ConfigurationFilesystem(VirtualFilesystem):
|
|
96
98
|
parts, entry = self._get_till_file(path, relentry)
|
97
99
|
|
98
100
|
if entry.is_dir():
|
99
|
-
return entry
|
101
|
+
return ConfigurationEntry(self, entry.path, entry, None)
|
100
102
|
|
101
103
|
entry = self._convert_entry(entry, *args, **kwargs)
|
102
104
|
|
@@ -108,23 +110,21 @@ class ConfigurationFilesystem(VirtualFilesystem):
|
|
108
110
|
|
109
111
|
return entry
|
110
112
|
|
111
|
-
def _convert_entry(
|
112
|
-
self, file_entry: FilesystemEntry, *args, **kwargs
|
113
|
-
) -> Union[ConfigurationEntry, FilesystemEntry]:
|
113
|
+
def _convert_entry(self, file_entry: FilesystemEntry, *args, **kwargs) -> ConfigurationEntry:
|
114
114
|
"""Creates a :class:`ConfigurationEntry` from a ``file_entry``.
|
115
115
|
|
116
116
|
If an error occurs during the parsing of the file contents,
|
117
117
|
the original ``file_entry`` is returned.
|
118
118
|
"""
|
119
119
|
entry = file_entry
|
120
|
+
config_parser = None
|
120
121
|
try:
|
121
122
|
config_parser = parse(entry, *args, **kwargs)
|
122
|
-
entry = ConfigurationEntry(self, entry.path, entry, config_parser)
|
123
123
|
except ConfigurationParsingError as e:
|
124
124
|
# If a parsing error gets created, it should return the `entry`
|
125
125
|
log.debug("Error when parsing %s with message '%s'", entry.path, e)
|
126
126
|
|
127
|
-
return entry
|
127
|
+
return ConfigurationEntry(self, entry.path, entry, config_parser)
|
128
128
|
|
129
129
|
|
130
130
|
class ConfigurationEntry(FilesystemEntry):
|
@@ -163,10 +163,10 @@ class ConfigurationEntry(FilesystemEntry):
|
|
163
163
|
|
164
164
|
def __init__(
|
165
165
|
self,
|
166
|
-
fs:
|
166
|
+
fs: ConfigurationFilesystem,
|
167
167
|
path: str,
|
168
168
|
entry: FilesystemEntry,
|
169
|
-
parser_items:
|
169
|
+
parser_items: dict | ConfigurationParser | str | list | None = None,
|
170
170
|
) -> None:
|
171
171
|
super().__init__(fs, path, entry)
|
172
172
|
self.parser_items = parser_items
|
@@ -182,7 +182,7 @@ class ConfigurationEntry(FilesystemEntry):
|
|
182
182
|
|
183
183
|
return f"<{self.__class__.__name__} {output}"
|
184
184
|
|
185
|
-
def get(self, key, default:
|
185
|
+
def get(self, key, default: Any | None = None) -> ConfigurationEntry | Any | None:
|
186
186
|
"""Gets the dictionary key that belongs to this entry using ``key``.
|
187
187
|
Behaves like ``dictionary.get()``.
|
188
188
|
|
@@ -197,13 +197,19 @@ class ConfigurationEntry(FilesystemEntry):
|
|
197
197
|
if not key:
|
198
198
|
raise TypeError("key should be defined")
|
199
199
|
|
200
|
-
|
200
|
+
path = fsutil.join(self.path, key, alt_separator=self.fs.alt_separator)
|
201
|
+
|
202
|
+
if self.parser_items and key in self.parser_items:
|
201
203
|
return ConfigurationEntry(
|
202
204
|
self.fs,
|
203
|
-
|
205
|
+
path,
|
204
206
|
self.entry,
|
205
207
|
self.parser_items[key],
|
206
208
|
)
|
209
|
+
|
210
|
+
if self.entry.is_dir():
|
211
|
+
return self.fs.get(path, self.entry)
|
212
|
+
|
207
213
|
return default
|
208
214
|
|
209
215
|
def _write_value_mapping(self, values: dict[str, Any], indentation_nr: int = 0) -> str:
|
@@ -257,6 +263,11 @@ class ConfigurationEntry(FilesystemEntry):
|
|
257
263
|
if self.is_file():
|
258
264
|
raise NotADirectoryError()
|
259
265
|
|
266
|
+
if self.parser_items is None and self.entry.is_dir():
|
267
|
+
for entry in self.entry.scandir():
|
268
|
+
yield ConfigurationEntry(self.fs, entry.name, entry, None)
|
269
|
+
return
|
270
|
+
|
260
271
|
for key, values in self.parser_items.items():
|
261
272
|
yield ConfigurationEntry(self.fs, key, self.entry, values)
|
262
273
|
|
@@ -266,7 +277,7 @@ class ConfigurationEntry(FilesystemEntry):
|
|
266
277
|
def is_dir(self, follow_symlinks: bool = True) -> bool:
|
267
278
|
"""Returns whether this :class:`ConfigurationEntry` can be considered a directory."""
|
268
279
|
# if self.parser_items has keys (thus sub-values), we can consider it a directory.
|
269
|
-
return hasattr(self.parser_items, "keys")
|
280
|
+
return (self.parser_items is None and self.entry.is_dir()) or hasattr(self.parser_items, "keys")
|
270
281
|
|
271
282
|
def is_symlink(self) -> bool:
|
272
283
|
"""Return whether this :class:`ConfigurationEntry` is a symlink or not.
|
@@ -284,7 +295,7 @@ class ConfigurationEntry(FilesystemEntry):
|
|
284
295
|
Returns:
|
285
296
|
Whether the ``entry`` and ``key`` exists
|
286
297
|
"""
|
287
|
-
return self.entry.exists() and key in self.parser_items
|
298
|
+
return self.parser_items and self.entry.exists() and key in self.parser_items
|
288
299
|
|
289
300
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
290
301
|
"""Returns the stat from the underlying :class:`.FilesystemEntry` :attr:`entry`."""
|
@@ -134,6 +134,10 @@ class ExtFilesystemEntry(FilesystemEntry):
|
|
134
134
|
st_info.st_mtime_ns = self.entry.mtime_ns
|
135
135
|
st_info.st_ctime_ns = self.entry.ctime_ns
|
136
136
|
|
137
|
+
# Set blocks
|
138
|
+
st_info.st_blocks = self.entry.inode.i_blocks_lo
|
139
|
+
st_info.st_blksize = self.entry.extfs.block_size
|
140
|
+
|
137
141
|
return st_info
|
138
142
|
|
139
143
|
def attr(self) -> Any:
|
@@ -94,7 +94,7 @@ class ITunesFilesystemEntry(VirtualFile):
|
|
94
94
|
def readlink_ext(self) -> FilesystemEntry:
|
95
95
|
"""Read the link if this entry is a symlink. Returns a filesystem entry."""
|
96
96
|
# Can't use the one in VirtualFile as it overrides the FilesystemEntry
|
97
|
-
return fsutil.resolve_link(
|
97
|
+
return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator)
|
98
98
|
|
99
99
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
100
100
|
"""Return the stat information of this entry."""
|
@@ -121,7 +121,7 @@ class TarFilesystemEntry(VirtualFile):
|
|
121
121
|
def readlink_ext(self) -> FilesystemEntry:
|
122
122
|
"""Read the link if this entry is a symlink. Returns a filesystem entry."""
|
123
123
|
# Can't use the one in VirtualFile as it overrides the FilesystemEntry
|
124
|
-
return fsutil.resolve_link(
|
124
|
+
return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator)
|
125
125
|
|
126
126
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
127
127
|
"""Return the stat information of this entry."""
|
@@ -4,16 +4,21 @@ import logging
|
|
4
4
|
import stat
|
5
5
|
import zipfile
|
6
6
|
from datetime import datetime, timezone
|
7
|
-
from typing import BinaryIO,
|
7
|
+
from typing import BinaryIO, Iterator
|
8
8
|
|
9
9
|
from dissect.util.stream import BufferedStream
|
10
10
|
|
11
|
-
from dissect.target.exceptions import
|
11
|
+
from dissect.target.exceptions import (
|
12
|
+
FileNotFoundError,
|
13
|
+
FilesystemError,
|
14
|
+
IsADirectoryError,
|
15
|
+
NotADirectoryError,
|
16
|
+
NotASymlinkError,
|
17
|
+
)
|
12
18
|
from dissect.target.filesystem import (
|
13
19
|
Filesystem,
|
14
20
|
FilesystemEntry,
|
15
21
|
VirtualDirectory,
|
16
|
-
VirtualFile,
|
17
22
|
VirtualFilesystem,
|
18
23
|
)
|
19
24
|
from dissect.target.helpers import fsutil
|
@@ -33,7 +38,7 @@ class ZipFilesystem(Filesystem):
|
|
33
38
|
def __init__(
|
34
39
|
self,
|
35
40
|
fh: BinaryIO,
|
36
|
-
base:
|
41
|
+
base: str | None = None,
|
37
42
|
*args,
|
38
43
|
**kwargs,
|
39
44
|
):
|
@@ -52,12 +57,7 @@ class ZipFilesystem(Filesystem):
|
|
52
57
|
continue
|
53
58
|
|
54
59
|
rel_name = fsutil.normpath(mname[len(self.base) :], alt_separator=self.alt_separator)
|
55
|
-
|
56
|
-
# NOTE: Normally we would check here if the member is a symlink or not
|
57
|
-
|
58
|
-
entry_cls = ZipFilesystemDirectoryEntry if member.is_dir() else ZipFilesystemEntry
|
59
|
-
file_entry = entry_cls(self, rel_name, member)
|
60
|
-
self._fs.map_file_entry(rel_name, file_entry)
|
60
|
+
self._fs.map_file_entry(rel_name, ZipFilesystemEntry(self, rel_name, member))
|
61
61
|
|
62
62
|
@staticmethod
|
63
63
|
def _detect(fh: BinaryIO) -> bool:
|
@@ -69,60 +69,95 @@ class ZipFilesystem(Filesystem):
|
|
69
69
|
return self._fs.get(path, relentry=relentry)
|
70
70
|
|
71
71
|
|
72
|
-
|
72
|
+
# Note: We subclass from VirtualDirectory because VirtualFilesystem is currently only compatible with VirtualDirectory
|
73
|
+
# Subclass from VirtualDirectory so we get that compatibility for free, and override the rest to do our own thing
|
74
|
+
class ZipFilesystemEntry(VirtualDirectory):
|
75
|
+
fs: ZipFilesystem
|
76
|
+
entry: zipfile.ZipInfo
|
77
|
+
|
78
|
+
def __init__(self, fs: ZipFilesystem, path: str, entry: zipfile.ZipInfo):
|
79
|
+
super().__init__(fs, path)
|
80
|
+
self.entry = entry
|
81
|
+
|
73
82
|
def open(self) -> BinaryIO:
|
74
|
-
|
83
|
+
if self.is_dir():
|
84
|
+
raise IsADirectoryError(self.path)
|
85
|
+
|
86
|
+
if self.is_symlink():
|
87
|
+
return self._resolve().open()
|
88
|
+
|
75
89
|
try:
|
76
90
|
return BufferedStream(self.fs.zip.open(self.entry), size=self.entry.file_size)
|
77
91
|
except Exception:
|
78
|
-
raise FileNotFoundError()
|
92
|
+
raise FileNotFoundError(self.path)
|
79
93
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
94
|
+
def iterdir(self) -> Iterator[str]:
|
95
|
+
if not self.is_dir():
|
96
|
+
raise NotADirectoryError(self.path)
|
83
97
|
|
84
|
-
|
85
|
-
|
86
|
-
|
98
|
+
entry = self._resolve()
|
99
|
+
if isinstance(entry, ZipFilesystemEntry):
|
100
|
+
yield from super(ZipFilesystemEntry, entry).iterdir()
|
101
|
+
else:
|
102
|
+
yield from entry.iterdir()
|
87
103
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
104
|
+
def scandir(self) -> Iterator[FilesystemEntry]:
|
105
|
+
if not self.is_dir():
|
106
|
+
raise NotADirectoryError(self.path)
|
91
107
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
stat.S_IFREG | 0o777,
|
98
|
-
self.entry.header_offset,
|
99
|
-
id(self.fs),
|
100
|
-
1,
|
101
|
-
0,
|
102
|
-
0,
|
103
|
-
self.entry.file_size,
|
104
|
-
0,
|
105
|
-
datetime(*self.entry.date_time, tzinfo=timezone.utc).timestamp(),
|
106
|
-
0,
|
107
|
-
]
|
108
|
-
)
|
108
|
+
entry = self._resolve()
|
109
|
+
if isinstance(entry, ZipFilesystemEntry):
|
110
|
+
yield from super(ZipFilesystemEntry, entry).scandir()
|
111
|
+
else:
|
112
|
+
yield from entry.scandir()
|
109
113
|
|
114
|
+
def is_dir(self, follow_symlinks: bool = True) -> bool:
|
115
|
+
try:
|
116
|
+
entry = self._resolve(follow_symlinks=follow_symlinks)
|
117
|
+
except FilesystemError:
|
118
|
+
return False
|
110
119
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
120
|
+
if isinstance(entry, ZipFilesystemEntry):
|
121
|
+
return entry.entry.is_dir()
|
122
|
+
return isinstance(entry, VirtualDirectory)
|
123
|
+
|
124
|
+
def is_file(self, follow_symlinks: bool = True) -> bool:
|
125
|
+
try:
|
126
|
+
entry = self._resolve(follow_symlinks=follow_symlinks)
|
127
|
+
except FilesystemError:
|
128
|
+
return False
|
129
|
+
|
130
|
+
if isinstance(entry, ZipFilesystemEntry):
|
131
|
+
return not entry.entry.is_dir()
|
132
|
+
return False
|
133
|
+
|
134
|
+
def is_symlink(self) -> bool:
|
135
|
+
return stat.S_ISLNK(self.entry.external_attr >> 16)
|
136
|
+
|
137
|
+
def readlink(self) -> str:
|
138
|
+
if not self.is_symlink():
|
139
|
+
raise NotASymlinkError()
|
140
|
+
return self.fs.zip.open(self.entry).read().decode()
|
141
|
+
|
142
|
+
def readlink_ext(self) -> FilesystemEntry:
|
143
|
+
return FilesystemEntry.readlink_ext(self)
|
115
144
|
|
116
145
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
117
|
-
|
118
|
-
return self.lstat()
|
146
|
+
return self._resolve(follow_symlinks=follow_symlinks).lstat()
|
119
147
|
|
120
148
|
def lstat(self) -> fsutil.stat_result:
|
121
149
|
"""Return the stat information of the given path, without resolving links."""
|
122
150
|
# ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
|
151
|
+
mode = self.entry.external_attr >> 16
|
152
|
+
|
153
|
+
if self.entry.is_dir() and not stat.S_ISDIR(mode):
|
154
|
+
mode = stat.S_IFDIR | mode
|
155
|
+
elif not self.entry.is_dir() and not stat.S_ISREG(mode):
|
156
|
+
mode = stat.S_IFREG | mode
|
157
|
+
|
123
158
|
return fsutil.stat_result(
|
124
159
|
[
|
125
|
-
|
160
|
+
mode,
|
126
161
|
self.entry.header_offset,
|
127
162
|
id(self.fs),
|
128
163
|
1,
|
dissect/target/helpers/config.py
CHANGED
@@ -1,27 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import ast
|
2
4
|
import importlib.machinery
|
3
5
|
import importlib.util
|
4
6
|
import logging
|
5
7
|
from pathlib import Path
|
6
8
|
from types import ModuleType
|
7
|
-
from typing import Optional, Union
|
8
9
|
|
9
10
|
log = logging.getLogger(__name__)
|
10
11
|
|
11
12
|
CONFIG_NAME = ".targetcfg.py"
|
12
13
|
|
13
14
|
|
14
|
-
def load(
|
15
|
+
def load(paths: list[Path | str] | Path | str | None) -> ModuleType:
|
16
|
+
"""Attempt to load one configuration from the provided path(s)."""
|
17
|
+
|
18
|
+
if isinstance(paths, Path) or isinstance(paths, str):
|
19
|
+
paths = [paths]
|
20
|
+
|
15
21
|
config_spec = importlib.machinery.ModuleSpec("config", None)
|
16
22
|
config = importlib.util.module_from_spec(config_spec)
|
17
|
-
config_file = _find_config_file(
|
23
|
+
config_file = _find_config_file(paths)
|
24
|
+
|
18
25
|
if config_file:
|
19
26
|
config_values = _parse_ast(config_file.read_bytes())
|
20
27
|
config.__dict__.update(config_values)
|
28
|
+
|
21
29
|
return config
|
22
30
|
|
23
31
|
|
24
|
-
def _parse_ast(code: str) -> dict[str,
|
32
|
+
def _parse_ast(code: str) -> dict[str, str | int]:
|
25
33
|
# Only allow basic value assignments for backwards compatibility
|
26
34
|
obj = {}
|
27
35
|
|
@@ -49,15 +57,19 @@ def _parse_ast(code: str) -> dict[str, Union[str, int]]:
|
|
49
57
|
return obj
|
50
58
|
|
51
59
|
|
52
|
-
def _find_config_file(
|
53
|
-
"""Find a config file anywhere in the given path and return it.
|
60
|
+
def _find_config_file(paths: list[Path | str] | None) -> Path | None:
|
61
|
+
"""Find a config file anywhere in the given path(s) and return it.
|
54
62
|
|
55
63
|
This algorithm allows parts of the path to not exist or the last part to be a filename.
|
56
64
|
It also does not look in the root directory ('/') for config files.
|
57
65
|
"""
|
58
66
|
|
67
|
+
if not paths:
|
68
|
+
return
|
69
|
+
|
59
70
|
config_file = None
|
60
|
-
|
71
|
+
|
72
|
+
for path in paths:
|
61
73
|
path = Path(path)
|
62
74
|
cur_path = path.absolute()
|
63
75
|
|
@@ -69,4 +81,7 @@ def _find_config_file(path: Optional[Union[Path, str]]) -> Optional[Path]:
|
|
69
81
|
config_file = cur_config
|
70
82
|
cur_path = cur_path.parent
|
71
83
|
|
84
|
+
if config_file:
|
85
|
+
break
|
86
|
+
|
72
87
|
return config_file
|