dissect.target 3.13.dev26__py3-none-any.whl → 3.14__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 +9 -1
- dissect/target/containers/asdf.py +2 -0
- dissect/target/containers/ewf.py +2 -0
- dissect/target/containers/hdd.py +2 -0
- dissect/target/containers/hds.py +2 -0
- dissect/target/containers/qcow2.py +2 -0
- dissect/target/containers/raw.py +2 -0
- dissect/target/containers/split.py +2 -0
- dissect/target/containers/vdi.py +2 -0
- dissect/target/containers/vhd.py +2 -0
- dissect/target/containers/vhdx.py +2 -0
- dissect/target/containers/vmdk.py +2 -0
- dissect/target/filesystem.py +108 -15
- dissect/target/filesystems/ad1.py +1 -1
- dissect/target/filesystems/btrfs.py +180 -0
- dissect/target/filesystems/cb.py +4 -4
- dissect/target/filesystems/config.py +161 -31
- dissect/target/filesystems/dir.py +1 -1
- dissect/target/filesystems/exfat.py +1 -1
- dissect/target/filesystems/extfs.py +5 -1
- dissect/target/filesystems/fat.py +1 -1
- dissect/target/filesystems/ffs.py +1 -1
- dissect/target/filesystems/itunes.py +1 -1
- dissect/target/filesystems/ntfs.py +1 -1
- dissect/target/filesystems/smb.py +1 -1
- dissect/target/filesystems/squashfs.py +1 -1
- dissect/target/filesystems/tar.py +1 -1
- dissect/target/filesystems/vmfs.py +1 -1
- dissect/target/filesystems/xfs.py +1 -1
- dissect/target/filesystems/zip.py +1 -1
- dissect/target/helpers/cache.py +2 -2
- dissect/target/helpers/configutil.py +283 -83
- dissect/target/helpers/fsutil.py +9 -6
- dissect/target/helpers/hashutil.py +20 -19
- dissect/target/helpers/utils.py +14 -3
- dissect/target/loaders/ad1.py +1 -1
- dissect/target/loaders/asdf.py +1 -1
- dissect/target/loaders/log.py +2 -2
- dissect/target/loaders/smb.py +23 -13
- dissect/target/loaders/targetd.py +12 -2
- dissect/target/loaders/vma.py +1 -1
- dissect/target/loaders/xva.py +1 -1
- dissect/target/plugin.py +14 -2
- dissect/target/plugins/apps/av/sophos.py +1 -2
- dissect/target/plugins/apps/av/symantec.py +3 -4
- dissect/target/plugins/apps/av/trendmicro.py +2 -3
- dissect/target/plugins/{browsers → apps/browser}/chrome.py +6 -3
- dissect/target/plugins/{browsers → apps/browser}/chromium.py +18 -13
- dissect/target/plugins/{browsers → apps/browser}/edge.py +6 -3
- dissect/target/plugins/{browsers → apps/browser}/firefox.py +3 -7
- dissect/target/plugins/{browsers → apps/browser}/iexplore.py +14 -4
- dissect/target/plugins/apps/remoteaccess/teamviewer.py +55 -27
- dissect/target/plugins/apps/ssh/opensshd.py +31 -30
- dissect/target/plugins/apps/{webservers → webserver}/apache.py +1 -1
- dissect/target/plugins/apps/{webservers → webserver}/caddy.py +1 -1
- dissect/target/plugins/apps/{webservers → webserver}/iis.py +1 -1
- dissect/target/plugins/apps/{webservers → webserver}/nginx.py +1 -1
- dissect/target/plugins/child/hyperv.py +1 -2
- dissect/target/plugins/child/vmware_workstation.py +1 -3
- dissect/target/plugins/filesystem/acquire_handles.py +2 -0
- dissect/target/plugins/filesystem/acquire_hash.py +1 -7
- dissect/target/plugins/filesystem/icat.py +5 -5
- dissect/target/plugins/filesystem/ntfs/mft.py +2 -2
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +2 -2
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -3
- dissect/target/plugins/filesystem/resolver.py +1 -1
- dissect/target/plugins/filesystem/unix/capability.py +77 -66
- dissect/target/plugins/filesystem/walkfs.py +25 -19
- dissect/target/plugins/filesystem/yara.py +20 -19
- dissect/target/plugins/general/config.py +28 -11
- dissect/target/plugins/os/unix/_os.py +28 -21
- dissect/target/plugins/os/unix/bsd/osx/user.py +1 -3
- dissect/target/plugins/os/unix/cronjobs.py +4 -16
- dissect/target/plugins/os/unix/{linux/esxi → esxi}/_os.py +5 -6
- dissect/target/plugins/os/unix/generic.py +5 -1
- dissect/target/plugins/os/unix/history.py +2 -1
- dissect/target/plugins/os/unix/linux/_os.py +12 -5
- dissect/target/plugins/os/unix/linux/services.py +112 -0
- dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -4
- dissect/target/plugins/os/unix/locale.py +3 -1
- dissect/target/plugins/os/unix/log/journal.py +7 -6
- dissect/target/plugins/os/unix/packagemanager.py +3 -3
- dissect/target/plugins/os/unix/shadow.py +1 -1
- dissect/target/plugins/os/windows/_os.py +2 -1
- dissect/target/plugins/os/windows/amcache.py +9 -10
- dissect/target/plugins/os/windows/catroot.py +2 -2
- dissect/target/plugins/os/windows/cim.py +5 -4
- dissect/target/plugins/os/windows/datetime.py +4 -1
- dissect/target/plugins/os/windows/defender.py +3 -3
- dissect/target/plugins/os/windows/generic.py +10 -11
- dissect/target/plugins/os/windows/lnk.py +6 -6
- dissect/target/plugins/os/windows/log/amcache.py +3 -5
- dissect/target/plugins/os/windows/log/pfro.py +1 -3
- dissect/target/plugins/os/windows/prefetch.py +5 -6
- dissect/target/plugins/os/windows/recyclebin.py +3 -4
- dissect/target/plugins/os/windows/regf/7zip.py +2 -4
- dissect/target/plugins/os/windows/regf/bam.py +1 -2
- dissect/target/plugins/os/windows/regf/cit.py +4 -5
- dissect/target/plugins/os/windows/regf/mru.py +6 -2
- dissect/target/plugins/os/windows/regf/muicache.py +1 -3
- dissect/target/plugins/os/windows/regf/recentfilecache.py +1 -2
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -2
- dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
- dissect/target/plugins/os/windows/regf/userassist.py +1 -2
- dissect/target/plugins/os/windows/services.py +2 -4
- dissect/target/plugins/os/windows/sru.py +4 -4
- dissect/target/plugins/os/windows/startupinfo.py +5 -6
- dissect/target/plugins/os/windows/syscache.py +2 -3
- dissect/target/target.py +65 -32
- dissect/target/tools/info.py +2 -1
- dissect/target/tools/mount.py +2 -12
- dissect/target/tools/shell.py +3 -2
- dissect/target/volume.py +10 -9
- dissect/target/volumes/bde.py +1 -1
- dissect/target/volumes/ddf.py +2 -0
- dissect/target/volumes/disk.py +2 -0
- dissect/target/volumes/luks.py +1 -1
- dissect/target/volumes/lvm.py +2 -0
- dissect/target/volumes/md.py +2 -0
- dissect/target/volumes/vmfs.py +2 -0
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/METADATA +2 -1
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/RECORD +137 -136
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/WHEEL +1 -1
- dissect/target/plugins/os/unix/services.py +0 -151
- /dissect/target/plugins/apps/{containers → browser}/__init__.py +0 -0
- /dissect/target/plugins/{browsers → apps/browser}/browser.py +0 -0
- /dissect/target/plugins/apps/{vpns → container}/__init__.py +0 -0
- /dissect/target/plugins/apps/{containers → container}/docker.py +0 -0
- /dissect/target/plugins/apps/{webservers → vpn}/__init__.py +0 -0
- /dissect/target/plugins/apps/{vpns → vpn}/openvpn.py +0 -0
- /dissect/target/plugins/apps/{vpns → vpn}/wireguard.py +0 -0
- /dissect/target/plugins/{browsers → apps/webserver}/__init__.py +0 -0
- /dissect/target/plugins/apps/{webservers/webservers.py → webserver/webserver.py} +0 -0
- /dissect/target/plugins/os/unix/{linux/esxi → esxi}/__init__.py +0 -0
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/LICENSE +0 -0
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,19 @@ from collections import deque
|
|
6
6
|
from configparser import ConfigParser, MissingSectionHeaderError
|
7
7
|
from dataclasses import dataclass
|
8
8
|
from fnmatch import fnmatch
|
9
|
-
from
|
9
|
+
from types import TracebackType
|
10
|
+
from typing import (
|
11
|
+
Any,
|
12
|
+
Callable,
|
13
|
+
ItemsView,
|
14
|
+
Iterable,
|
15
|
+
Iterator,
|
16
|
+
KeysView,
|
17
|
+
Literal,
|
18
|
+
Optional,
|
19
|
+
TextIO,
|
20
|
+
Union,
|
21
|
+
)
|
10
22
|
|
11
23
|
from dissect.target.exceptions import ConfigurationParsingError, FileNotFoundError
|
12
24
|
from dissect.target.filesystem import FilesystemEntry
|
@@ -58,18 +70,32 @@ class PeekableIterator:
|
|
58
70
|
|
59
71
|
|
60
72
|
class ConfigurationParser:
|
73
|
+
"""A configuration parser where you can configure certain aspects of the parsing mechanism.
|
74
|
+
|
75
|
+
Attributes:
|
76
|
+
parsed_data: The resulting dictionary after parsing.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
collapse: A ``bool`` or an ``Iterator``:
|
80
|
+
If ``True``: it will collapse all the resulting dictionary values.
|
81
|
+
If an ``Iterable`` it will collapse on the keys defined in ``collapse``.
|
82
|
+
collapse_inverse: Inverses the collapsing mechanism. Collapse on everything that is not inside ``collapse``.
|
83
|
+
separator: Contains what values it should look for as a separator.
|
84
|
+
comment_prefixes: Contains what constitutes as a comment.
|
85
|
+
"""
|
86
|
+
|
61
87
|
def __init__(
|
62
88
|
self,
|
63
|
-
collapse: Union[bool,
|
89
|
+
collapse: Union[bool, Iterable[str]] = False,
|
64
90
|
collapse_inverse: bool = False,
|
65
|
-
|
91
|
+
separator: tuple[str] = ("=",),
|
66
92
|
comment_prefixes: tuple[str] = (";", "#"),
|
67
93
|
) -> None:
|
68
94
|
self.collapse_all = collapse is True
|
69
|
-
self.collapse = collapse if isinstance(collapse,
|
95
|
+
self.collapse = set(collapse) if isinstance(collapse, Iterable) else set()
|
70
96
|
self._collapse_check = self._key_not_in_collapse if collapse_inverse else self._key_in_collapse
|
71
97
|
|
72
|
-
self.
|
98
|
+
self.separator = separator
|
73
99
|
self.comment_prefixes = comment_prefixes
|
74
100
|
self.parsed_data = {}
|
75
101
|
|
@@ -101,6 +127,13 @@ class ConfigurationParser:
|
|
101
127
|
return key not in self.collapse
|
102
128
|
|
103
129
|
def parse_file(self, fh: TextIO) -> None:
|
130
|
+
"""Parse the contents of ``fh`` into key/value pairs.
|
131
|
+
|
132
|
+
This function should **set** :attr:`parsed_data` as a side_effect.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
fh: The text to parse.
|
136
|
+
"""
|
104
137
|
raise NotImplementedError()
|
105
138
|
|
106
139
|
def get(self, item: str, default: Optional[Any] = None) -> Any:
|
@@ -110,7 +143,7 @@ class ConfigurationParser:
|
|
110
143
|
"""Parse a configuration file.
|
111
144
|
|
112
145
|
Raises:
|
113
|
-
ConfigurationParsingError: If any exception occurs during during the parsing process
|
146
|
+
ConfigurationParsingError: If any exception occurs during during the parsing process.
|
114
147
|
"""
|
115
148
|
|
116
149
|
try:
|
@@ -121,6 +154,9 @@ class ConfigurationParser:
|
|
121
154
|
if self.collapse_all or self.collapse:
|
122
155
|
self.parsed_data = self._collapse_dict(self.parsed_data)
|
123
156
|
|
157
|
+
if not isinstance(self.parsed_data, dict):
|
158
|
+
self.parsed_data = self._collapse_dict(self.parsed_data, False)
|
159
|
+
|
124
160
|
def keys(self) -> KeysView:
|
125
161
|
return self.parsed_data.keys()
|
126
162
|
|
@@ -129,19 +165,19 @@ class ConfigurationParser:
|
|
129
165
|
|
130
166
|
|
131
167
|
class Default(ConfigurationParser):
|
132
|
-
"""Parse a configuration file specified by ``
|
168
|
+
"""Parse a configuration file specified by ``separator`` and ``comment_prefixes``.
|
133
169
|
|
134
|
-
This parser splits only on the first ``
|
170
|
+
This parser splits only on the first ``separator`` it finds:
|
135
171
|
|
136
|
-
key<
|
172
|
+
key<separator>value -> {"key": "value"}
|
137
173
|
|
138
|
-
key<
|
174
|
+
key<separator>value\n
|
139
175
|
continuation
|
140
176
|
-> {"key": "value continuation"}
|
141
177
|
|
142
178
|
# Unless we collapse values, we add them to a list to not overwrite any values.
|
143
|
-
key<
|
144
|
-
key<
|
179
|
+
key<separator>value1
|
180
|
+
key<separator>value2
|
145
181
|
-> {key: [value1, value2]}
|
146
182
|
|
147
183
|
<empty_space><comment> -> skip
|
@@ -149,17 +185,18 @@ class Default(ConfigurationParser):
|
|
149
185
|
|
150
186
|
def __init__(self, *args, **kwargs) -> None:
|
151
187
|
super().__init__(*args, **kwargs)
|
152
|
-
self.
|
188
|
+
self.SEPARATOR = re.compile(rf"\s*[{''.join(self.separator)}]\s*")
|
153
189
|
self.COMMENTS = re.compile(rf"\s*[{''.join(self.comment_prefixes)}]")
|
154
190
|
self.skip_lines = self.comment_prefixes + ("\n",)
|
155
191
|
|
156
|
-
def line_reader(self, fh: TextIO) -> Iterator[str]:
|
192
|
+
def line_reader(self, fh: TextIO, strip_comments: bool = True) -> Iterator[str]:
|
157
193
|
for line in fh:
|
158
194
|
if line.strip().startswith(self.skip_lines) or not line.strip():
|
159
195
|
continue
|
160
196
|
|
161
|
-
|
162
|
-
|
197
|
+
if strip_comments:
|
198
|
+
line, *_ = self.COMMENTS.split(line, 1)
|
199
|
+
|
163
200
|
yield line
|
164
201
|
|
165
202
|
def parse_file(self, fh: TextIO) -> None:
|
@@ -173,7 +210,7 @@ class Default(ConfigurationParser):
|
|
173
210
|
information_dict[prev_key] = " ".join([prev_value, line.strip()])
|
174
211
|
continue
|
175
212
|
|
176
|
-
prev_key, *value = self.
|
213
|
+
prev_key, *value = self.SEPARATOR.split(line, 1)
|
177
214
|
value = value[0].strip() if value else ""
|
178
215
|
|
179
216
|
_update_dictionary(information_dict, prev_key, value)
|
@@ -189,7 +226,7 @@ class Ini(ConfigurationParser):
|
|
189
226
|
|
190
227
|
self.parsed_data = ConfigParser(
|
191
228
|
strict=False,
|
192
|
-
delimiters=self.
|
229
|
+
delimiters=self.separator,
|
193
230
|
comment_prefixes=self.comment_prefixes,
|
194
231
|
allow_no_value=True,
|
195
232
|
interpolation=None,
|
@@ -217,11 +254,86 @@ class Txt(ConfigurationParser):
|
|
217
254
|
self.parsed_data = {"content": fh.read(), "size": str(fh.tell())}
|
218
255
|
|
219
256
|
|
257
|
+
class ScopeManager:
|
258
|
+
"""A (context)manager for dictionary scoping.
|
259
|
+
|
260
|
+
This class provides utility functions to keep track of scopes inside a dictionary.
|
261
|
+
|
262
|
+
Attributes:
|
263
|
+
_parents: A dictionary accounting what child belongs to which parent dictionary.
|
264
|
+
_root: The initial dictionary.
|
265
|
+
_current: The current dictionary.
|
266
|
+
_previous: The node before the current (changed) node.
|
267
|
+
"""
|
268
|
+
|
269
|
+
def __init__(self) -> None:
|
270
|
+
self._parents = {}
|
271
|
+
self._root = {}
|
272
|
+
self._current = self._root
|
273
|
+
self._previous = None
|
274
|
+
|
275
|
+
def __enter__(self) -> ScopeManager:
|
276
|
+
return self
|
277
|
+
|
278
|
+
def __exit__(
|
279
|
+
self,
|
280
|
+
type: Optional[type[BaseException]],
|
281
|
+
value: Optional[BaseException],
|
282
|
+
traceback: Optional[TracebackType],
|
283
|
+
) -> None:
|
284
|
+
self.clean()
|
285
|
+
|
286
|
+
def _set_prev(self, keep_prev: bool) -> None:
|
287
|
+
"""Set :attr:`_previous` before :attr:`_current` changes."""
|
288
|
+
if not keep_prev:
|
289
|
+
self._previous = self._current
|
290
|
+
|
291
|
+
def push(self, name: str, keep_prev: bool = False) -> Literal[True]:
|
292
|
+
"""Push a new key to the :attr:`_current` dictionary and return that we did."""
|
293
|
+
child = self._current.get(name, {})
|
294
|
+
|
295
|
+
parent = self._current
|
296
|
+
self._parents[id(child)] = parent
|
297
|
+
parent[name] = child
|
298
|
+
self._set_prev(keep_prev)
|
299
|
+
self._current = child
|
300
|
+
return True
|
301
|
+
|
302
|
+
def pop(self, keep_prev: bool = False) -> bool:
|
303
|
+
"""Pop :attr:`_current` and return whether we changed the :attr:`_parents` dictionary."""
|
304
|
+
if new_current := self._parents.pop(id(self._current), None):
|
305
|
+
self._set_prev(keep_prev)
|
306
|
+
self._current = new_current
|
307
|
+
return True
|
308
|
+
return False
|
309
|
+
|
310
|
+
def update(self, key: str, value: str) -> None:
|
311
|
+
"""Update the :attr:`_current` dictionary with ``key`` and ``value``."""
|
312
|
+
_update_dictionary(self._current, key, value)
|
313
|
+
|
314
|
+
def update_prev(self, key: str, value: str) -> None:
|
315
|
+
"""Update the :attr:`_previous` dictionary with ``key`` and ``value``."""
|
316
|
+
_update_dictionary(self._previous, key, value)
|
317
|
+
|
318
|
+
def is_root(self) -> bool:
|
319
|
+
"""Utility function to check whether the current dictionary is a root dictionary."""
|
320
|
+
return id(self._current) == id(self._root)
|
321
|
+
|
322
|
+
def clean(self) -> None:
|
323
|
+
"""Clean up the internal state.
|
324
|
+
This is called automatically when :class:`ScopeManager` is used as a contextmanager.
|
325
|
+
"""
|
326
|
+
self._parents = {}
|
327
|
+
self._root = {}
|
328
|
+
self._current = self._root
|
329
|
+
self._previous = None
|
330
|
+
|
331
|
+
|
220
332
|
class Indentation(Default):
|
221
|
-
"""This parser is used for
|
333
|
+
"""This parser is used for files that use a single level of indentation to specify a different scope.
|
222
334
|
|
223
|
-
Examples of these files are
|
224
|
-
Where "Match"
|
335
|
+
Examples of these files are the ``sshd_config`` file.
|
336
|
+
Where "Match" statements use a single layer of indentation to specify a scope for the key value pairs.
|
225
337
|
|
226
338
|
The parser parses this as the following:
|
227
339
|
|
@@ -230,80 +342,166 @@ class Indentation(Default):
|
|
230
342
|
-> {"key value": {"key2": "value2"}}
|
231
343
|
"""
|
232
344
|
|
233
|
-
def __init__(self, *args, **kwargs) -> None:
|
234
|
-
super().__init__(*args, **kwargs)
|
235
|
-
self._parents = {}
|
236
|
-
self._indentation = 0
|
237
|
-
|
238
345
|
def _parse_line(self, line: str) -> tuple[str, str]:
|
239
|
-
key, *value = self.
|
346
|
+
key, *value = self.SEPARATOR.split(line.strip(), 1)
|
240
347
|
value = value[0].strip() if value else ""
|
241
348
|
return key, value
|
242
349
|
|
243
|
-
def _push_scope(self, name: str, current: dict[str, Union[str, dict]]) -> dict[str, Union[str, dict]]:
|
244
|
-
child = current.get(name, {})
|
245
|
-
|
246
|
-
parent = current
|
247
|
-
self._parents[id(child)] = parent
|
248
|
-
parent[name] = child
|
249
|
-
return child
|
250
|
-
|
251
|
-
def _pop_scope(self, current: dict[str, Union[str, dict]]) -> dict[str, Union[str, dict]]:
|
252
|
-
self._indentation = 0
|
253
|
-
return self._parents.pop(id(current), current)
|
254
|
-
|
255
350
|
def _change_scope(
|
256
351
|
self,
|
352
|
+
manager: ScopeManager,
|
257
353
|
line: str,
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
354
|
+
key: str,
|
355
|
+
next_line: Optional[str] = None,
|
356
|
+
) -> bool:
|
357
|
+
"""A function to check whether to create a new scope, or go back to a previous one.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
manager: A :class:`ScopeManager` that contains the logic to ``push`` and ``pop`` scopes. And keeps state.
|
361
|
+
line: The line to be parsed.
|
362
|
+
key: The key that should be updated during a :method:`ScopeManager.push``.
|
363
|
+
next_line: The next line to be parsed.
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
Whether the scope changed or not.
|
367
|
+
"""
|
262
368
|
empty_space = (" ", "\t")
|
369
|
+
changed = False
|
263
370
|
|
264
371
|
if next_line is None:
|
265
|
-
return
|
372
|
+
return False
|
266
373
|
|
267
374
|
if not line.startswith(empty_space):
|
268
|
-
|
375
|
+
changed = manager.pop()
|
269
376
|
|
270
377
|
if not line.startswith(empty_space) and next_line.startswith(empty_space):
|
271
|
-
|
272
|
-
|
273
|
-
return current
|
378
|
+
return manager.push(key)
|
379
|
+
return changed
|
274
380
|
|
275
381
|
def parse_file(self, fh: TextIO) -> None:
|
276
|
-
root = {}
|
277
|
-
current = root
|
278
|
-
|
279
382
|
iterator = PeekableIterator(self.line_reader(fh))
|
280
383
|
prev_key = None
|
281
|
-
for line in iterator:
|
282
|
-
key, value = self._parse_line(line)
|
283
|
-
prev_dict = current
|
284
|
-
current = self._change_scope(line, iterator.peek(), line.strip(), current)
|
285
384
|
|
286
|
-
|
287
|
-
|
288
|
-
|
385
|
+
with ScopeManager() as manager:
|
386
|
+
for line in iterator:
|
387
|
+
key, value = self._parse_line(line)
|
388
|
+
changed = self._change_scope(
|
389
|
+
manager=manager,
|
390
|
+
line=line,
|
391
|
+
key=line.strip(),
|
392
|
+
next_line=iterator.peek(),
|
393
|
+
)
|
394
|
+
|
395
|
+
if changed:
|
396
|
+
prev_key = line.strip()
|
397
|
+
continue
|
398
|
+
|
399
|
+
if not value:
|
400
|
+
key, value = prev_key, key
|
401
|
+
manager.pop()
|
402
|
+
|
403
|
+
manager.update(key, value)
|
404
|
+
|
405
|
+
self.parsed_data = manager._root
|
406
|
+
|
407
|
+
|
408
|
+
class SystemD(Indentation):
|
409
|
+
"""A :class:`ConfigurationParser` that specifically parses systemd configuration files.
|
410
|
+
|
411
|
+
Examples:
|
412
|
+
>>> systemd_data = textwrap.dedent(
|
413
|
+
'''
|
414
|
+
[Section1]
|
415
|
+
Key=Value
|
416
|
+
[Section2]
|
417
|
+
Key2=Value 2\\
|
418
|
+
Value 2 continued
|
419
|
+
'''
|
420
|
+
)
|
421
|
+
>>> parser = SystemD(io.StringIO(systemd_data))
|
422
|
+
>>> parser.parser_items
|
423
|
+
{
|
424
|
+
"Section1": {
|
425
|
+
"Key": "Value
|
426
|
+
},
|
427
|
+
"Section2": {
|
428
|
+
"Key2": "Value2 Value 2 continued
|
429
|
+
}
|
430
|
+
}
|
431
|
+
|
432
|
+
"""
|
289
433
|
|
290
|
-
|
291
|
-
|
292
|
-
|
434
|
+
def _change_scope(
|
435
|
+
self,
|
436
|
+
manager: ScopeManager,
|
437
|
+
line: str,
|
438
|
+
key: str,
|
439
|
+
next_line: Optional[str] = None,
|
440
|
+
) -> bool:
|
441
|
+
scope_char = ("[", "]")
|
442
|
+
changed = False
|
443
|
+
if line.startswith(scope_char):
|
444
|
+
if not manager.is_root():
|
445
|
+
changed = manager.pop()
|
446
|
+
stripped_characters = "".join(scope_char)
|
447
|
+
changed = manager.push(key.strip(stripped_characters), changed)
|
448
|
+
|
449
|
+
return changed
|
293
450
|
|
294
|
-
|
451
|
+
def parse_file(self, fh: TextIO) -> None:
|
452
|
+
prev_values = []
|
453
|
+
prev_key = None
|
295
454
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
455
|
+
with ScopeManager() as manager:
|
456
|
+
for line in self.line_reader(fh, strip_comments=False):
|
457
|
+
changed = self._change_scope(
|
458
|
+
manager=manager,
|
459
|
+
line=line,
|
460
|
+
key=line.strip(),
|
461
|
+
)
|
462
|
+
|
463
|
+
if changed:
|
464
|
+
# Current part is a section header.
|
465
|
+
if prev_values:
|
466
|
+
# Update previous key/value... someone configured it wrong
|
467
|
+
prev_values, prev_key = self._update_continued_values(
|
468
|
+
func=manager.update_prev,
|
469
|
+
key=prev_key,
|
470
|
+
values=prev_values,
|
471
|
+
)
|
472
|
+
continue
|
473
|
+
|
474
|
+
key, value = self._parse_line(line)
|
475
|
+
|
476
|
+
continued_value = value or key
|
477
|
+
if continued_value.endswith("\\"):
|
478
|
+
prev_key = prev_key or key
|
479
|
+
prev_values.append(continued_value.strip("\\ "))
|
480
|
+
continue
|
481
|
+
|
482
|
+
if prev_values:
|
483
|
+
prev_values, prev_key = self._update_continued_values(
|
484
|
+
func=manager.update,
|
485
|
+
key=prev_key,
|
486
|
+
values=prev_values + [continued_value],
|
487
|
+
)
|
488
|
+
continue
|
489
|
+
|
490
|
+
manager.update(key, value)
|
491
|
+
|
492
|
+
self.parsed_data = manager._root
|
493
|
+
|
494
|
+
def _update_continued_values(self, func: Callable, key, values: list[str]) -> tuple[list, None]:
|
495
|
+
value = " ".join(values)
|
496
|
+
func(key, value)
|
497
|
+
return [], None
|
300
498
|
|
301
499
|
|
302
500
|
@dataclass(frozen=True)
|
303
501
|
class ParserOptions:
|
304
502
|
collapse: Optional[Union[bool, set]] = None
|
305
503
|
collapse_inverse: Optional[bool] = None
|
306
|
-
|
504
|
+
separator: Optional[tuple[str]] = None
|
307
505
|
comment_prefixes: Optional[tuple[str]] = None
|
308
506
|
|
309
507
|
|
@@ -312,13 +510,13 @@ class ParserConfig:
|
|
312
510
|
parser: type[ConfigurationParser] = Default
|
313
511
|
collapse: Optional[Union[bool, set]] = None
|
314
512
|
collapse_inverse: Optional[bool] = None
|
315
|
-
|
513
|
+
separator: Optional[tuple[str]] = None
|
316
514
|
comment_prefixes: Optional[tuple[str]] = None
|
317
515
|
|
318
516
|
def create_parser(self, options: Optional[ParserOptions] = None) -> ConfigurationParser:
|
319
517
|
kwargs = {}
|
320
518
|
|
321
|
-
for field_name in ["collapse", "collapse_inverse", "
|
519
|
+
for field_name in ["collapse", "collapse_inverse", "separator", "comment_prefixes"]:
|
322
520
|
value = getattr(options, field_name, None) or getattr(self, field_name)
|
323
521
|
if value:
|
324
522
|
kwargs.update({field_name: value})
|
@@ -327,28 +525,29 @@ class ParserConfig:
|
|
327
525
|
|
328
526
|
|
329
527
|
MATCH_MAP: dict[str, ParserConfig] = {
|
330
|
-
"*/systemd/*": ParserConfig(
|
528
|
+
"*/systemd/*": ParserConfig(SystemD),
|
331
529
|
"*/sysconfig/network-scripts/ifcfg-*": ParserConfig(Default),
|
332
530
|
"*/sysctl.d/*.conf": ParserConfig(Default),
|
333
531
|
}
|
334
532
|
|
335
|
-
|
336
533
|
CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
|
337
534
|
"ini": ParserConfig(Ini),
|
338
535
|
"xml": ParserConfig(Txt),
|
339
536
|
"json": ParserConfig(Txt),
|
340
537
|
"cnf": ParserConfig(Default),
|
341
|
-
"conf": ParserConfig(Default,
|
538
|
+
"conf": ParserConfig(Default, separator=(r"\s",)),
|
342
539
|
"sample": ParserConfig(Txt),
|
540
|
+
"systemd": ParserConfig(SystemD),
|
343
541
|
"template": ParserConfig(Txt),
|
344
542
|
}
|
543
|
+
|
345
544
|
KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
|
346
545
|
"ulogd.conf": ParserConfig(Ini),
|
347
|
-
"sshd_config": ParserConfig(Indentation,
|
348
|
-
"hosts.allow": ParserConfig(Default,
|
349
|
-
"hosts.deny": ParserConfig(Default,
|
350
|
-
"hosts": ParserConfig(Default,
|
351
|
-
"nsswitch.conf": ParserConfig(Default,
|
546
|
+
"sshd_config": ParserConfig(Indentation, separator=(r"\s",)),
|
547
|
+
"hosts.allow": ParserConfig(Default, separator=(":",), comment_prefixes=("#",)),
|
548
|
+
"hosts.deny": ParserConfig(Default, separator=(":",), comment_prefixes=("#",)),
|
549
|
+
"hosts": ParserConfig(Default, separator=(r"\s",)),
|
550
|
+
"nsswitch.conf": ParserConfig(Default, separator=(":",)),
|
352
551
|
"lsb-release": ParserConfig(Default),
|
353
552
|
}
|
354
553
|
|
@@ -357,17 +556,18 @@ def parse(path: Union[FilesystemEntry, TargetPath], hint: Optional[str] = None,
|
|
357
556
|
"""Parses the content of an ``path`` or ``entry`` to a dictionary.
|
358
557
|
|
359
558
|
Args:
|
360
|
-
|
361
|
-
hint:
|
362
|
-
collapse:
|
363
|
-
|
364
|
-
|
559
|
+
path: The path to either a directory or file.
|
560
|
+
hint: What kind of parser should be used.
|
561
|
+
collapse: Whether it should collapse everything or just a certain set of keys.
|
562
|
+
collapse_inverse: Invert the collapse function to collapse everything but the keys inside ``collapse``.
|
563
|
+
separator: The separator that should be used for parsing.
|
564
|
+
comment_prefixes: What is specified as a comment.
|
365
565
|
|
366
566
|
Raises:
|
367
567
|
FileNotFoundError: If the ``path`` is not a file.
|
368
568
|
"""
|
369
569
|
|
370
|
-
if not path.is_file():
|
570
|
+
if not path.is_file(follow_symlinks=True):
|
371
571
|
raise FileNotFoundError(f"Could not parse {path} as a dictionary.")
|
372
572
|
|
373
573
|
entry = path
|
dissect/target/helpers/fsutil.py
CHANGED
@@ -725,12 +725,15 @@ class TargetPath(Path, PureDissectPath):
|
|
725
725
|
|
726
726
|
def open(self, mode='rb', buffering=0, encoding=None,
|
727
727
|
errors=None, newline=None):
|
728
|
-
|
729
|
-
if "b" not in mode
|
730
|
-
|
731
|
-
#
|
732
|
-
|
733
|
-
|
728
|
+
|
729
|
+
if "b" not in mode:
|
730
|
+
encoding = encoding or "UTF-8"
|
731
|
+
# CPython >= 3.10
|
732
|
+
if hasattr(io, "text_encoding"):
|
733
|
+
# Vermin linting needs to be skipped for this line as this is
|
734
|
+
# guarded by an explicit check for availability.
|
735
|
+
# novermin
|
736
|
+
encoding = io.text_encoding(encoding)
|
734
737
|
return self._accessor.open(self, mode, buffering, encoding, errors,
|
735
738
|
newline)
|
736
739
|
|
@@ -15,13 +15,9 @@ if TYPE_CHECKING:
|
|
15
15
|
|
16
16
|
BUFFER_SIZE = 32768
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
("path[]", "paths"),
|
22
|
-
("digest[]", "digests"),
|
23
|
-
],
|
24
|
-
)
|
18
|
+
RECORD_NAME = "filesystem/file/digest"
|
19
|
+
NAME_SUFFIXES = ["_resolved", "_digest"]
|
20
|
+
RECORD_TYPES = ["path", "digest"]
|
25
21
|
|
26
22
|
|
27
23
|
def _hash(fh: BinaryIO, ctx: Union[HASH, list[HASH]]) -> tuple[str]:
|
@@ -76,12 +72,8 @@ def hash_uri_records(target: Target, record: Record) -> Record:
|
|
76
72
|
|
77
73
|
def hash_path_records(target: Target, record: Record) -> Record:
|
78
74
|
"""Hash files from path fields inside the record."""
|
79
|
-
hashed_paths = []
|
80
75
|
|
81
|
-
|
82
|
-
path_type = fieldtypes.windows_path
|
83
|
-
else:
|
84
|
-
path_type = fieldtypes.posix_path
|
76
|
+
hash_records = []
|
85
77
|
|
86
78
|
for field_name, field_type in record._field_types.items():
|
87
79
|
if not issubclass(field_type, fieldtypes.path):
|
@@ -97,16 +89,25 @@ def hash_path_records(target: Target, record: Record) -> Record:
|
|
97
89
|
except (FileNotFoundError, IsADirectoryError):
|
98
90
|
pass
|
99
91
|
else:
|
100
|
-
resolved_path =
|
101
|
-
|
92
|
+
resolved_path = target.fs.path(resolved_path)
|
93
|
+
record_kwargs = dict()
|
94
|
+
record_def = list()
|
102
95
|
|
103
|
-
|
104
|
-
|
96
|
+
fields = [resolved_path, path_hash]
|
97
|
+
|
98
|
+
for type, name, field in zip(RECORD_TYPES, NAME_SUFFIXES, fields):
|
99
|
+
hashed_field_name = f"{field_name}{name}"
|
100
|
+
record_kwargs.update({hashed_field_name: field})
|
101
|
+
record_def.append((type, hashed_field_name))
|
105
102
|
|
106
|
-
|
107
|
-
|
103
|
+
_record = RecordDescriptor(RECORD_NAME, record_def)
|
104
|
+
|
105
|
+
hash_records.append(_record(**record_kwargs))
|
106
|
+
|
107
|
+
if not hash_records:
|
108
|
+
return record
|
108
109
|
|
109
|
-
return GroupedRecord(record._desc.name, [record
|
110
|
+
return GroupedRecord(record._desc.name, [record] + hash_records)
|
110
111
|
|
111
112
|
|
112
113
|
def hash_uri(target: Target, path: str) -> tuple[str, str]:
|
dissect/target/helpers/utils.py
CHANGED
@@ -4,7 +4,7 @@ import urllib.parse
|
|
4
4
|
from datetime import datetime, timezone, tzinfo
|
5
5
|
from enum import Enum
|
6
6
|
from pathlib import Path
|
7
|
-
from typing import BinaryIO, Iterator, Union
|
7
|
+
from typing import BinaryIO, Callable, Iterator, Optional, Union
|
8
8
|
|
9
9
|
from dissect.util.ts import from_unix
|
10
10
|
|
@@ -17,7 +17,7 @@ class StrEnum(str, Enum):
|
|
17
17
|
"""Sortable and serializible string-based enum"""
|
18
18
|
|
19
19
|
|
20
|
-
def list_to_frozen_set(function):
|
20
|
+
def list_to_frozen_set(function: Callable) -> Callable:
|
21
21
|
def wrapper(*args):
|
22
22
|
args = [frozenset(x) if isinstance(x, list) else x for x in args]
|
23
23
|
return function(*args)
|
@@ -25,7 +25,7 @@ def list_to_frozen_set(function):
|
|
25
25
|
return wrapper
|
26
26
|
|
27
27
|
|
28
|
-
def parse_path_uri(path):
|
28
|
+
def parse_path_uri(path: Path) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
29
29
|
if path is None:
|
30
30
|
return None, None, None
|
31
31
|
parsed_path = urllib.parse.urlparse(str(path))
|
@@ -33,6 +33,17 @@ def parse_path_uri(path):
|
|
33
33
|
return parsed_path.scheme, parsed_path.path, parsed_query
|
34
34
|
|
35
35
|
|
36
|
+
def parse_options_string(options: str) -> dict[str, Union[str, bool]]:
|
37
|
+
result = {}
|
38
|
+
for opt in options.split(","):
|
39
|
+
if "=" in opt:
|
40
|
+
key, _, value = opt.partition("=")
|
41
|
+
result[key] = value
|
42
|
+
else:
|
43
|
+
result[opt] = True
|
44
|
+
return result
|
45
|
+
|
46
|
+
|
36
47
|
SLUG_RE = re.compile(r"[/\\ ]")
|
37
48
|
|
38
49
|
|