dissect.target 3.16.dev45__py3-none-any.whl → 3.17__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 -0
- dissect/target/containers/fortifw.py +190 -0
- dissect/target/filesystem.py +192 -67
- dissect/target/filesystems/dir.py +14 -1
- dissect/target/filesystems/overlay.py +103 -0
- dissect/target/helpers/compat/path_common.py +19 -5
- dissect/target/helpers/configutil.py +30 -7
- dissect/target/helpers/network_managers.py +101 -73
- dissect/target/helpers/record_modifier.py +4 -1
- dissect/target/loader.py +3 -1
- dissect/target/loaders/dir.py +23 -5
- dissect/target/loaders/itunes.py +3 -3
- dissect/target/loaders/mqtt.py +309 -0
- dissect/target/loaders/overlay.py +31 -0
- dissect/target/loaders/target.py +12 -9
- dissect/target/loaders/vb.py +2 -2
- dissect/target/loaders/velociraptor.py +5 -4
- dissect/target/plugin.py +1 -1
- dissect/target/plugins/apps/browser/brave.py +10 -0
- dissect/target/plugins/apps/browser/browser.py +43 -0
- dissect/target/plugins/apps/browser/chrome.py +10 -0
- dissect/target/plugins/apps/browser/chromium.py +234 -12
- dissect/target/plugins/apps/browser/edge.py +10 -0
- dissect/target/plugins/apps/browser/firefox.py +512 -19
- dissect/target/plugins/apps/browser/iexplore.py +2 -2
- dissect/target/plugins/apps/container/docker.py +24 -4
- dissect/target/plugins/apps/ssh/openssh.py +4 -0
- dissect/target/plugins/apps/ssh/putty.py +45 -14
- dissect/target/plugins/apps/ssh/ssh.py +40 -0
- dissect/target/plugins/apps/vpn/openvpn.py +115 -93
- dissect/target/plugins/child/docker.py +24 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
- dissect/target/plugins/filesystem/walkfs.py +2 -2
- dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
- dissect/target/plugins/os/unix/esxi/_os.py +2 -2
- dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
- dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
- dissect/target/plugins/os/unix/linux/services.py +1 -0
- dissect/target/plugins/os/unix/linux/sockets.py +2 -2
- dissect/target/plugins/os/unix/log/messages.py +53 -8
- dissect/target/plugins/os/windows/_os.py +10 -1
- dissect/target/plugins/os/windows/catroot.py +178 -63
- dissect/target/plugins/os/windows/credhist.py +210 -0
- dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
- dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
- dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
- dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
- dissect/target/plugins/os/windows/sam.py +10 -1
- dissect/target/target.py +1 -1
- dissect/target/tools/dump/run.py +23 -28
- dissect/target/tools/dump/state.py +11 -8
- dissect/target/tools/dump/utils.py +5 -4
- dissect/target/tools/query.py +3 -15
- dissect/target/tools/shell.py +48 -8
- dissect/target/tools/utils.py +23 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/RECORD +62 -55
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,12 @@ import hashlib
|
|
4
4
|
import hmac
|
5
5
|
from typing import Optional, Union
|
6
6
|
|
7
|
-
|
7
|
+
try:
|
8
|
+
from Crypto.Cipher import AES, ARC4
|
9
|
+
|
10
|
+
HAS_CRYPTO = True
|
11
|
+
except ImportError:
|
12
|
+
HAS_CRYPTO = False
|
8
13
|
|
9
14
|
CIPHER_ALGORITHMS: dict[Union[int, str], CipherAlgorithm] = {}
|
10
15
|
HASH_ALGORITHMS: dict[Union[int, str], HashAlgorithm] = {}
|
@@ -62,6 +67,9 @@ class _AES(CipherAlgorithm):
|
|
62
67
|
block_length = 128 // 8
|
63
68
|
|
64
69
|
def decrypt(self, data: bytes, key: bytes, iv: Optional[bytes] = None) -> bytes:
|
70
|
+
if not HAS_CRYPTO:
|
71
|
+
raise RuntimeError("Missing pycryptodome dependency")
|
72
|
+
|
65
73
|
cipher = AES.new(
|
66
74
|
key[: self.key_length], mode=AES.MODE_CBC, IV=iv[: self.iv_length] if iv else b"\x00" * self.iv_length
|
67
75
|
)
|
@@ -93,6 +101,9 @@ class _RC4(CipherAlgorithm):
|
|
93
101
|
block_length = 1 // 8
|
94
102
|
|
95
103
|
def decrypt(self, data: bytes, key: bytes, iv: Optional[bytes] = None) -> bytes:
|
104
|
+
if not HAS_CRYPTO:
|
105
|
+
raise RuntimeError("Missing pycryptodome dependency")
|
106
|
+
|
96
107
|
cipher = ARC4.new(key[: self.key_length])
|
97
108
|
return cipher.decrypt(data)
|
98
109
|
|
@@ -1,21 +1,27 @@
|
|
1
1
|
import hashlib
|
2
2
|
import re
|
3
|
-
from functools import cached_property
|
3
|
+
from functools import cache, cached_property
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
|
-
|
6
|
+
try:
|
7
|
+
from Crypto.Cipher import AES
|
8
|
+
|
9
|
+
HAS_CRYPTO = True
|
10
|
+
except ImportError:
|
11
|
+
HAS_CRYPTO = False
|
12
|
+
|
7
13
|
|
8
|
-
from dissect.target import Target
|
9
14
|
from dissect.target.exceptions import UnsupportedPluginError
|
15
|
+
from dissect.target.helpers import keychain
|
10
16
|
from dissect.target.plugin import InternalPlugin
|
11
17
|
from dissect.target.plugins.os.windows.dpapi.blob import Blob as DPAPIBlob
|
12
18
|
from dissect.target.plugins.os.windows.dpapi.master_key import CredSystem, MasterKeyFile
|
19
|
+
from dissect.target.target import Target
|
13
20
|
|
14
21
|
|
15
22
|
class DPAPIPlugin(InternalPlugin):
|
16
23
|
__namespace__ = "dpapi"
|
17
24
|
|
18
|
-
# This matches master key file names
|
19
25
|
MASTER_KEY_REGEX = re.compile("^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$")
|
20
26
|
|
21
27
|
SECURITY_POLICY_KEY = "HKEY_LOCAL_MACHINE\\SECURITY\\Policy"
|
@@ -25,11 +31,26 @@ class DPAPIPlugin(InternalPlugin):
|
|
25
31
|
|
26
32
|
def __init__(self, target: Target):
|
27
33
|
super().__init__(target)
|
34
|
+
self.keychain = cache(self.keychain)
|
28
35
|
|
29
36
|
def check_compatible(self) -> None:
|
37
|
+
if not HAS_CRYPTO:
|
38
|
+
raise UnsupportedPluginError("Missing pycryptodome dependency")
|
39
|
+
|
30
40
|
if not list(self.target.registry.keys(self.SYSTEM_KEY)):
|
31
41
|
raise UnsupportedPluginError(f"Registry key not found: {self.SYSTEM_KEY}")
|
32
42
|
|
43
|
+
def keychain(self) -> set:
|
44
|
+
passwords = set()
|
45
|
+
|
46
|
+
for key in keychain.get_keys_for_provider("user") + keychain.get_keys_without_provider():
|
47
|
+
if key.key_type == keychain.KeyType.PASSPHRASE:
|
48
|
+
passwords.add(key.value)
|
49
|
+
|
50
|
+
# It is possible to encrypt using an empty passphrase.
|
51
|
+
passwords.add("")
|
52
|
+
return passwords
|
53
|
+
|
33
54
|
@cached_property
|
34
55
|
def syskey(self) -> bytes:
|
35
56
|
lsa = self.target.registry.key(self.SYSTEM_KEY)
|
@@ -84,6 +105,10 @@ class DPAPIPlugin(InternalPlugin):
|
|
84
105
|
|
85
106
|
return result
|
86
107
|
|
108
|
+
@cached_property
|
109
|
+
def _users(self) -> dict[str, dict[str, str]]:
|
110
|
+
return {u.name: {"sid": u.sid} for u in self.target.users()}
|
111
|
+
|
87
112
|
def _load_master_keys_from_path(self, username: str, path: Path) -> dict[str, MasterKeyFile]:
|
88
113
|
if not path.exists():
|
89
114
|
return {}
|
@@ -104,21 +129,51 @@ class DPAPIPlugin(InternalPlugin):
|
|
104
129
|
if not mkf.decrypted:
|
105
130
|
raise Exception("Failed to decrypt System master key")
|
106
131
|
|
132
|
+
if user := self._users.get(username):
|
133
|
+
for mk_pass in self.keychain():
|
134
|
+
if mkf.decrypt_with_password(user["sid"], mk_pass):
|
135
|
+
break
|
136
|
+
|
137
|
+
try:
|
138
|
+
if mkf.decrypt_with_hash(user["sid"], bytes.fromhex(mk_pass)) is True:
|
139
|
+
break
|
140
|
+
except ValueError:
|
141
|
+
pass
|
142
|
+
|
143
|
+
if not mkf.decrypted:
|
144
|
+
self.target.log.warning("Could not decrypt DPAPI master key for username '%s'", username)
|
145
|
+
|
107
146
|
result[file.name] = mkf
|
108
147
|
|
109
148
|
return result
|
110
149
|
|
111
150
|
def decrypt_system_blob(self, data: bytes) -> bytes:
|
151
|
+
"""Decrypt the given bytes using the System master key."""
|
152
|
+
return self.decrypt_user_blob(data, self.SYSTEM_USERNAME)
|
153
|
+
|
154
|
+
def decrypt_user_blob(self, data: bytes, username: str) -> bytes:
|
155
|
+
"""Decrypt the given bytes using the master key of the given user."""
|
112
156
|
blob = DPAPIBlob(data)
|
113
157
|
|
114
|
-
if not (mk := self.master_keys.get(
|
115
|
-
raise ValueError("Blob UUID is unknown to
|
158
|
+
if not (mk := self.master_keys.get(username, {}).get(blob.guid)):
|
159
|
+
raise ValueError(f"Blob UUID is unknown to {username} master keys")
|
116
160
|
|
117
161
|
if not blob.decrypt(mk.key):
|
118
|
-
raise ValueError("Failed to decrypt
|
162
|
+
raise ValueError(f"Failed to decrypt blob for user {username}")
|
119
163
|
|
120
164
|
return blob.clear_text
|
121
165
|
|
166
|
+
def decrypt_blob(self, data: bytes) -> bytes:
|
167
|
+
"""Attempt to decrypt the given bytes using any of the available master keys."""
|
168
|
+
blob = DPAPIBlob(data)
|
169
|
+
|
170
|
+
for user in self.master_keys:
|
171
|
+
for mk in self.master_keys[user].values():
|
172
|
+
if blob.decrypt(mk.key):
|
173
|
+
return blob.clear_text
|
174
|
+
|
175
|
+
raise ValueError("Failed to decrypt blob")
|
176
|
+
|
122
177
|
|
123
178
|
def _decrypt_aes(data: bytes, key: bytes) -> bytes:
|
124
179
|
ctx = hashlib.sha256()
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import hashlib
|
2
|
+
import logging
|
2
3
|
from io import BytesIO
|
3
4
|
from typing import BinaryIO
|
4
5
|
|
@@ -11,6 +12,16 @@ from dissect.target.plugins.os.windows.dpapi.crypto import (
|
|
11
12
|
dpapi_hmac,
|
12
13
|
)
|
13
14
|
|
15
|
+
try:
|
16
|
+
from Crypto.Hash import MD4
|
17
|
+
|
18
|
+
HAS_CRYPTO = True
|
19
|
+
except ImportError:
|
20
|
+
HAS_CRYPTO = False
|
21
|
+
|
22
|
+
log = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
14
25
|
master_key_def = """
|
15
26
|
struct DomainKey {
|
16
27
|
DWORD dwVersion;
|
@@ -85,9 +96,18 @@ class MasterKey:
|
|
85
96
|
|
86
97
|
def decrypt_with_password(self, user_sid: str, pwd: str) -> bool:
|
87
98
|
"""Decrypts the master key with the given user's password and SID."""
|
99
|
+
pwd = pwd.encode("utf-16-le")
|
100
|
+
|
88
101
|
for algo in ["sha1", "md4"]:
|
89
|
-
|
90
|
-
|
102
|
+
if algo in hashlib.algorithms_available:
|
103
|
+
pwd_hash = hashlib.new(algo, pwd)
|
104
|
+
elif HAS_CRYPTO and algo == "md4":
|
105
|
+
pwd_hash = MD4.new(pwd)
|
106
|
+
else:
|
107
|
+
log.warning("No cryptography capabilities for algorithm %s", algo)
|
108
|
+
continue
|
109
|
+
|
110
|
+
self.decrypt_with_key(derive_password_hash(pwd_hash.digest(), user_sid))
|
91
111
|
if self.decrypted:
|
92
112
|
break
|
93
113
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Iterator
|
2
|
+
|
1
3
|
from dissect.target.exceptions import UnsupportedPluginError
|
2
4
|
from dissect.target.helpers.descriptor_extensions import (
|
3
5
|
RegistryRecordDescriptorExtension,
|
@@ -11,7 +13,7 @@ RunKeyRecord = create_extended_descriptor([RegistryRecordDescriptorExtension, Us
|
|
11
13
|
[
|
12
14
|
("datetime", "ts"),
|
13
15
|
("wstring", "name"),
|
14
|
-
("
|
16
|
+
("command", "command"),
|
15
17
|
("string", "key"),
|
16
18
|
],
|
17
19
|
)
|
@@ -48,7 +50,7 @@ class RunKeysPlugin(Plugin):
|
|
48
50
|
raise UnsupportedPluginError("No registry run key found")
|
49
51
|
|
50
52
|
@export(record=RunKeyRecord)
|
51
|
-
def runkeys(self):
|
53
|
+
def runkeys(self) -> Iterator[RunKeyRecord]:
|
52
54
|
"""Iterate various run key locations. See source for all locations.
|
53
55
|
|
54
56
|
Run keys (Run and RunOnce) are registry keys that make a program run when a user logs on. a Run key runs every
|
@@ -63,7 +65,7 @@ class RunKeysPlugin(Plugin):
|
|
63
65
|
domain (string): The target domain.
|
64
66
|
ts (datetime): The registry key last modified timestamp.
|
65
67
|
name (string): The run key name.
|
66
|
-
|
68
|
+
command (command): The run key command.
|
67
69
|
key (string): The source key for this run key.
|
68
70
|
"""
|
69
71
|
for key in self.KEYS:
|
@@ -73,7 +75,7 @@ class RunKeysPlugin(Plugin):
|
|
73
75
|
yield RunKeyRecord(
|
74
76
|
ts=r.ts,
|
75
77
|
name=entry.name,
|
76
|
-
|
78
|
+
command=entry.value,
|
77
79
|
key=key,
|
78
80
|
_target=self.target,
|
79
81
|
_key=r,
|
@@ -2,7 +2,13 @@ from hashlib import md5, sha256
|
|
2
2
|
from struct import pack
|
3
3
|
from typing import Iterator
|
4
4
|
|
5
|
-
|
5
|
+
try:
|
6
|
+
from Crypto.Cipher import AES, ARC4, DES
|
7
|
+
|
8
|
+
HAS_CRYPTO = True
|
9
|
+
except ImportError:
|
10
|
+
HAS_CRYPTO = False
|
11
|
+
|
6
12
|
from dissect import cstruct
|
7
13
|
from dissect.util import ts
|
8
14
|
|
@@ -295,6 +301,9 @@ class SamPlugin(Plugin):
|
|
295
301
|
SAM_KEY = "HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account"
|
296
302
|
|
297
303
|
def check_compatible(self) -> None:
|
304
|
+
if not HAS_CRYPTO:
|
305
|
+
raise UnsupportedPluginError("Missing pycryptodome dependency")
|
306
|
+
|
298
307
|
if not len(list(self.target.registry.keys(self.SAM_KEY))) > 0:
|
299
308
|
raise UnsupportedPluginError(f"Registry key not found: {self.SAM_KEY}")
|
300
309
|
|
dissect/target/target.py
CHANGED
@@ -280,7 +280,7 @@ class Target:
|
|
280
280
|
continue
|
281
281
|
|
282
282
|
getlogger(entry).debug("Attempting to use loader: %s", loader_cls)
|
283
|
-
for sub_entry in loader_cls.find_all(entry):
|
283
|
+
for sub_entry in loader_cls.find_all(entry, parsed_path=parsed_path):
|
284
284
|
try:
|
285
285
|
ldr = loader_cls(sub_entry, parsed_path=parsed_path)
|
286
286
|
except Exception as e:
|
dissect/target/tools/dump/run.py
CHANGED
@@ -7,7 +7,7 @@ import sys
|
|
7
7
|
from collections import deque
|
8
8
|
from dataclasses import dataclass
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Any,
|
10
|
+
from typing import Any, Iterable, Iterator, Optional
|
11
11
|
|
12
12
|
import structlog
|
13
13
|
from flow.record import Record
|
@@ -25,10 +25,12 @@ from dissect.target.tools.dump.utils import (
|
|
25
25
|
Compression,
|
26
26
|
Serialization,
|
27
27
|
cached_sink_writers,
|
28
|
-
get_nested_attr,
|
29
28
|
)
|
30
29
|
from dissect.target.tools.utils import (
|
30
|
+
PluginFunction,
|
31
31
|
configure_generic_arguments,
|
32
|
+
execute_function_on_target,
|
33
|
+
find_and_filter_plugins,
|
32
34
|
process_generic_arguments,
|
33
35
|
)
|
34
36
|
|
@@ -44,13 +46,13 @@ class RecordStreamElement:
|
|
44
46
|
sink_path: Optional[Path] = None
|
45
47
|
|
46
48
|
|
47
|
-
def get_targets(targets:
|
49
|
+
def get_targets(targets: list[str]) -> Iterator[Target]:
|
48
50
|
"""Return a generator with `Target` objects for provided paths"""
|
49
51
|
for target in Target.open_all(targets):
|
50
52
|
yield target
|
51
53
|
|
52
54
|
|
53
|
-
def execute_function(target: Target, function:
|
55
|
+
def execute_function(target: Target, function: PluginFunction) -> TargetRecordDescriptor:
|
54
56
|
"""
|
55
57
|
Execute function `function` on provided target `target` and return a generator
|
56
58
|
with the records produced.
|
@@ -62,7 +64,7 @@ def execute_function(target: Target, function: str) -> Generator[TargetRecordDes
|
|
62
64
|
local_log.debug("Function execution")
|
63
65
|
|
64
66
|
try:
|
65
|
-
target_attr =
|
67
|
+
output_type, target_attr, _ = execute_function_on_target(target, function)
|
66
68
|
except UnsupportedPluginError:
|
67
69
|
local_log.error("Function is not supported for target", exc_info=True)
|
68
70
|
return
|
@@ -70,15 +72,8 @@ def execute_function(target: Target, function: str) -> Generator[TargetRecordDes
|
|
70
72
|
local_log.error("Plugin error while executing function for target", exc_info=True)
|
71
73
|
return
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
output = getattr(target_attr, "__output__", "default") if hasattr(target_attr, "__output__") else None
|
76
|
-
except PluginError as e:
|
77
|
-
local_log.error("Plugin error while fetching an attribute", exc_info=e)
|
78
|
-
return
|
79
|
-
|
80
|
-
if output != "record":
|
81
|
-
local_log.warn("Output format is not supported", output=output)
|
75
|
+
if output_type != "record":
|
76
|
+
local_log.warn("Output format is not supported", output=output_type)
|
82
77
|
return
|
83
78
|
|
84
79
|
# no support for function-specific arguments
|
@@ -94,9 +89,9 @@ def execute_function(target: Target, function: str) -> Generator[TargetRecordDes
|
|
94
89
|
|
95
90
|
def produce_target_func_pairs(
|
96
91
|
targets: Iterable[Target],
|
97
|
-
functions:
|
92
|
+
functions: str,
|
98
93
|
state: DumpState,
|
99
|
-
) ->
|
94
|
+
) -> Iterator[tuple[Target, PluginFunction]]:
|
100
95
|
"""
|
101
96
|
Return a generator with target and function pairs for execution.
|
102
97
|
|
@@ -107,20 +102,20 @@ def produce_target_func_pairs(
|
|
107
102
|
pairs_to_skip.update((str(sink.target_path), sink.func) for sink in state.finished_sinks)
|
108
103
|
|
109
104
|
for target in targets:
|
110
|
-
for
|
111
|
-
if state and (target.path,
|
105
|
+
for func_def in find_and_filter_plugins(target, functions):
|
106
|
+
if state and (target.path, func_def.name) in pairs_to_skip:
|
112
107
|
log.info(
|
113
108
|
"Skipping target/func pair since its marked as done in provided state",
|
114
109
|
target=target.path,
|
115
|
-
func=
|
110
|
+
func=func_def.name,
|
116
111
|
state=state.path,
|
117
112
|
)
|
118
113
|
continue
|
119
|
-
yield (target,
|
120
|
-
state.mark_as_finished(target,
|
114
|
+
yield (target, func_def)
|
115
|
+
state.mark_as_finished(target, func_def.name)
|
121
116
|
|
122
117
|
|
123
|
-
def execute_functions(target_func_stream: Iterable[
|
118
|
+
def execute_functions(target_func_stream: Iterable[tuple[Target, str]]) -> Iterable[RecordStreamElement]:
|
124
119
|
"""
|
125
120
|
Execute a function on a target for target / function pairs in the stream.
|
126
121
|
|
@@ -131,7 +126,7 @@ def execute_functions(target_func_stream: Iterable[Tuple[Target, str]]) -> Gener
|
|
131
126
|
yield RecordStreamElement(target=target, func=func, record=record)
|
132
127
|
|
133
128
|
|
134
|
-
def log_progress(stream: Iterable[Any], step_size: int = 1000) ->
|
129
|
+
def log_progress(stream: Iterable[Any], step_size: int = 1000) -> Iterable[Any]:
|
135
130
|
"""
|
136
131
|
Log a number of items that went though the generator stream
|
137
132
|
after every N element (N is configured in `step_size`).
|
@@ -155,7 +150,7 @@ def log_progress(stream: Iterable[Any], step_size: int = 1000) -> Generator[Any,
|
|
155
150
|
def sink_records(
|
156
151
|
record_stream: Iterable[RecordStreamElement],
|
157
152
|
state: DumpState,
|
158
|
-
) ->
|
153
|
+
) -> Iterator[RecordStreamElement]:
|
159
154
|
"""
|
160
155
|
Persist records from the stream into appropriate sinks, per serialization, compression and record type.
|
161
156
|
"""
|
@@ -168,7 +163,7 @@ def sink_records(
|
|
168
163
|
def persist_processing_state(
|
169
164
|
record_stream: Iterable[RecordStreamElement],
|
170
165
|
state: DumpState,
|
171
|
-
) ->
|
166
|
+
) -> Iterator[RecordStreamElement]:
|
172
167
|
"""
|
173
168
|
Keep track of the pipeline state in a persistent state object.
|
174
169
|
"""
|
@@ -179,8 +174,8 @@ def persist_processing_state(
|
|
179
174
|
|
180
175
|
|
181
176
|
def execute_pipeline(
|
182
|
-
targets:
|
183
|
-
functions:
|
177
|
+
targets: list[str],
|
178
|
+
functions: str,
|
184
179
|
output_dir: Path,
|
185
180
|
serialization: Serialization,
|
186
181
|
compression: Optional[Compression] = None,
|
@@ -297,7 +292,7 @@ def main():
|
|
297
292
|
try:
|
298
293
|
execute_pipeline(
|
299
294
|
targets=args.targets,
|
300
|
-
functions=args.function
|
295
|
+
functions=args.function,
|
301
296
|
output_dir=args.output,
|
302
297
|
serialization=Serialization(args.serialization),
|
303
298
|
compression=Compression(args.compression),
|
@@ -6,7 +6,7 @@ import json
|
|
6
6
|
from contextlib import contextmanager
|
7
7
|
from dataclasses import dataclass
|
8
8
|
from pathlib import Path
|
9
|
-
from typing import Any, Callable, Iterator,
|
9
|
+
from typing import Any, Callable, Iterator, Optional, TextIO
|
10
10
|
|
11
11
|
import structlog
|
12
12
|
|
@@ -35,17 +35,20 @@ class Sink:
|
|
35
35
|
record_count: int = 0
|
36
36
|
size_bytes: int = 0
|
37
37
|
|
38
|
+
def __post_init__(self):
|
39
|
+
self.func = getattr(self.func, "name", self.func)
|
40
|
+
|
38
41
|
|
39
42
|
@dataclass
|
40
43
|
class DumpState:
|
41
|
-
target_paths:
|
42
|
-
functions:
|
44
|
+
target_paths: list[str]
|
45
|
+
functions: list[str]
|
43
46
|
serialization: str
|
44
47
|
compression: str
|
45
48
|
start_time: datetime.datetime
|
46
49
|
last_update_time: datetime.datetime
|
47
50
|
|
48
|
-
sinks:
|
51
|
+
sinks: list[Sink] = dataclasses.field(default_factory=list)
|
49
52
|
|
50
53
|
# Volatile properties
|
51
54
|
output_dir: Optional[Path] = None
|
@@ -56,7 +59,7 @@ class DumpState:
|
|
56
59
|
return sum(s.record_count for s in self.sinks)
|
57
60
|
|
58
61
|
@property
|
59
|
-
def finished_sinks(self) ->
|
62
|
+
def finished_sinks(self) -> list[Sink]:
|
60
63
|
return [sink for sink in self.sinks if not sink.is_dirty]
|
61
64
|
|
62
65
|
@property
|
@@ -178,7 +181,7 @@ class DumpState:
|
|
178
181
|
state.output_dir = output_dir
|
179
182
|
return state
|
180
183
|
|
181
|
-
def get_invalid_sinks(self) ->
|
184
|
+
def get_invalid_sinks(self) -> list[Sink]:
|
182
185
|
"""Return sinks that have a mismatch between recorded size and a real file size"""
|
183
186
|
invalid_sinks = []
|
184
187
|
for sink in self.sinks:
|
@@ -214,8 +217,8 @@ class DumpState:
|
|
214
217
|
def create_state(
|
215
218
|
*,
|
216
219
|
output_dir: Path,
|
217
|
-
target_paths:
|
218
|
-
functions:
|
220
|
+
target_paths: list[str],
|
221
|
+
functions: list[str],
|
219
222
|
serialization: Serialization,
|
220
223
|
compression: Compression = None,
|
221
224
|
) -> DumpState:
|
@@ -32,6 +32,7 @@ from flow.record.adapter.jsonfile import JsonfileWriter
|
|
32
32
|
from flow.record.jsonpacker import JsonRecordPacker
|
33
33
|
|
34
34
|
from dissect.target import Target
|
35
|
+
from dissect.target.plugin import PluginFunction
|
35
36
|
|
36
37
|
log = structlog.get_logger(__name__)
|
37
38
|
|
@@ -69,14 +70,14 @@ def get_nested_attr(obj: Any, nested_attr: str) -> Any:
|
|
69
70
|
|
70
71
|
|
71
72
|
@lru_cache(maxsize=DEST_DIR_CACHE_SIZE)
|
72
|
-
def get_sink_dir_by_target(target: Target, function:
|
73
|
-
func_first_name, _, _ = function.partition(".")
|
73
|
+
def get_sink_dir_by_target(target: Target, function: PluginFunction) -> Path:
|
74
|
+
func_first_name, _, _ = function.name.partition(".")
|
74
75
|
return Path(target.name) / func_first_name
|
75
76
|
|
76
77
|
|
77
78
|
@functools.lru_cache(maxsize=DEST_DIR_CACHE_SIZE)
|
78
|
-
def get_sink_dir_by_func(target: Target, function:
|
79
|
-
func_first_name, _, _ = function.partition(".")
|
79
|
+
def get_sink_dir_by_func(target: Target, function: PluginFunction) -> Path:
|
80
|
+
func_first_name, _, _ = function.name.partition(".")
|
80
81
|
return Path(func_first_name) / target.name
|
81
82
|
|
82
83
|
|
dissect/target/tools/query.py
CHANGED
@@ -26,6 +26,7 @@ from dissect.target.tools.utils import (
|
|
26
26
|
catch_sigpipe,
|
27
27
|
configure_generic_arguments,
|
28
28
|
execute_function_on_target,
|
29
|
+
find_and_filter_plugins,
|
29
30
|
generate_argparse_for_bound_method,
|
30
31
|
generate_argparse_for_plugin_class,
|
31
32
|
generate_argparse_for_unbound_method,
|
@@ -172,8 +173,7 @@ def main():
|
|
172
173
|
collected_plugins = {}
|
173
174
|
|
174
175
|
if targets:
|
175
|
-
for
|
176
|
-
plugin_target = Target.open(target)
|
176
|
+
for plugin_target in Target.open_all(targets, args.children):
|
177
177
|
if isinstance(plugin_target._loader, ProxyLoader):
|
178
178
|
parser.error("can't list compatible plugins for remote targets.")
|
179
179
|
funcs, _ = find_plugin_functions(plugin_target, args.list, compatibility=True, show_hidden=True)
|
@@ -270,25 +270,13 @@ def main():
|
|
270
270
|
basic_entries = []
|
271
271
|
yield_entries = []
|
272
272
|
|
273
|
-
# Keep a set of plugins that were already executed on the target.
|
274
|
-
executed_plugins = set()
|
275
|
-
|
276
273
|
first_seen_output_type = default_output_type
|
277
274
|
cli_params_unparsed = rest
|
278
275
|
|
279
|
-
func_defs, _ = find_plugin_functions(target, args.function, compatibility=False)
|
280
276
|
excluded_funcs, _ = find_plugin_functions(target, args.excluded_functions, compatibility=False)
|
281
277
|
excluded_func_paths = {excluded_func.path for excluded_func in excluded_funcs}
|
282
278
|
|
283
|
-
for func_def in
|
284
|
-
if func_def.path in excluded_func_paths:
|
285
|
-
continue
|
286
|
-
|
287
|
-
# Avoid executing same plugin for multiple OSes (like hostname)
|
288
|
-
if func_def.name in executed_plugins:
|
289
|
-
continue
|
290
|
-
executed_plugins.add(func_def.name)
|
291
|
-
|
279
|
+
for func_def in find_and_filter_plugins(target, args.function, excluded_func_paths):
|
292
280
|
# If the default type is record (meaning we skip everything else)
|
293
281
|
# and actual output type is not record, continue.
|
294
282
|
# We perform this check here because plugins that require output files/dirs
|
dissect/target/tools/shell.py
CHANGED
@@ -31,12 +31,13 @@ from dissect.target.exceptions import (
|
|
31
31
|
RegistryValueNotFoundError,
|
32
32
|
TargetError,
|
33
33
|
)
|
34
|
-
from dissect.target.filesystem import FilesystemEntry,
|
34
|
+
from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry
|
35
35
|
from dissect.target.helpers import cyber, fsutil, regutil
|
36
36
|
from dissect.target.plugin import arg
|
37
37
|
from dissect.target.target import Target
|
38
38
|
from dissect.target.tools.info import print_target_info
|
39
39
|
from dissect.target.tools.utils import (
|
40
|
+
args_to_uri,
|
40
41
|
catch_sigpipe,
|
41
42
|
configure_generic_arguments,
|
42
43
|
generate_argparse_for_bound_method,
|
@@ -468,7 +469,7 @@ class TargetCli(TargetCmd):
|
|
468
469
|
# If we happen to scan an NTFS filesystem see if any of the
|
469
470
|
# entries has an alternative data stream and also list them.
|
470
471
|
entry = file_.get()
|
471
|
-
if isinstance(entry,
|
472
|
+
if isinstance(entry, LayerFilesystemEntry):
|
472
473
|
if entry.entries.fs.__type__ == "ntfs":
|
473
474
|
attrs = entry.lattr()
|
474
475
|
for data_stream in attrs.DATA:
|
@@ -511,34 +512,66 @@ class TargetCli(TargetCmd):
|
|
511
512
|
@arg("-l", action="store_true")
|
512
513
|
@arg("-a", "--all", action="store_true") # ignored but included for proper argument parsing
|
513
514
|
@arg("-h", "--human-readable", action="store_true")
|
515
|
+
@arg("-R", "--recursive", action="store_true", help="recursively list subdirectories encountered")
|
516
|
+
@arg("-c", action="store_true", dest="use_ctime", help="show time when file status was last changed")
|
517
|
+
@arg("-u", action="store_true", dest="use_atime", help="show time of last access")
|
514
518
|
def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> Optional[bool]:
|
515
519
|
"""list directory contents"""
|
516
520
|
|
517
521
|
path = self.resolve_path(args.path)
|
518
522
|
|
523
|
+
if args.use_ctime and args.use_atime:
|
524
|
+
print("can't specify -c and -u at the same time")
|
525
|
+
return
|
526
|
+
|
519
527
|
if not path or not path.exists():
|
520
528
|
return
|
521
529
|
|
530
|
+
self._print_ls(args, path, 0, stdout)
|
531
|
+
|
532
|
+
def _print_ls(self, args: argparse.Namespace, path: fsutil.TargetPath, depth: int, stdout: TextIO) -> None:
|
533
|
+
path = self.resolve_path(path)
|
534
|
+
subdirs = []
|
535
|
+
|
522
536
|
if path.is_dir():
|
523
537
|
contents = self.scandir(path, color=True)
|
524
538
|
elif path.is_file():
|
525
539
|
contents = [(path, path.name)]
|
526
540
|
|
541
|
+
if depth > 0:
|
542
|
+
print(f"\n{str(path)}:", file=stdout)
|
543
|
+
|
527
544
|
if not args.l:
|
528
|
-
|
545
|
+
for target_path, name in contents:
|
546
|
+
print(name, file=stdout)
|
547
|
+
if target_path.is_dir():
|
548
|
+
subdirs.append(target_path)
|
529
549
|
else:
|
530
550
|
if len(contents) > 1:
|
531
551
|
print(f"total {len(contents)}", file=stdout)
|
532
552
|
for target_path, name in contents:
|
533
|
-
self.print_extensive_file_stat(stdout=stdout, target_path=target_path, name=name)
|
553
|
+
self.print_extensive_file_stat(args=args, stdout=stdout, target_path=target_path, name=name)
|
554
|
+
if target_path.is_dir():
|
555
|
+
subdirs.append(target_path)
|
556
|
+
|
557
|
+
if args.recursive and subdirs:
|
558
|
+
for subdir in subdirs:
|
559
|
+
self._print_ls(args, subdir, depth + 1, stdout)
|
534
560
|
|
535
|
-
def print_extensive_file_stat(
|
561
|
+
def print_extensive_file_stat(
|
562
|
+
self, args: argparse.Namespace, stdout: TextIO, target_path: fsutil.TargetPath, name: str
|
563
|
+
) -> None:
|
536
564
|
"""Print the file status."""
|
537
565
|
try:
|
538
566
|
entry = target_path.get()
|
539
567
|
stat = entry.lstat()
|
540
568
|
symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
|
541
|
-
|
569
|
+
show_time = stat.st_mtime
|
570
|
+
if args.use_ctime:
|
571
|
+
show_time = stat.st_ctime
|
572
|
+
elif args.use_atime:
|
573
|
+
show_time = stat.st_atime
|
574
|
+
utc_time = datetime.datetime.utcfromtimestamp(show_time).isoformat()
|
542
575
|
|
543
576
|
print(
|
544
577
|
f"{stat_modestr(stat)} {stat.st_uid:4d} {stat.st_gid:4d} {stat.st_size:6d} {utc_time} {name}{symlink}",
|
@@ -1223,10 +1256,17 @@ def main() -> None:
|
|
1223
1256
|
parser.add_argument("targets", metavar="TARGETS", nargs="*", help="targets to load")
|
1224
1257
|
parser.add_argument("-p", "--python", action="store_true", help="(I)Python shell")
|
1225
1258
|
parser.add_argument("-r", "--registry", action="store_true", help="registry shell")
|
1259
|
+
parser.add_argument(
|
1260
|
+
"-L",
|
1261
|
+
"--loader",
|
1262
|
+
action="store",
|
1263
|
+
default=None,
|
1264
|
+
help="select a specific loader (i.e. vmx, raw)",
|
1265
|
+
)
|
1226
1266
|
|
1227
1267
|
configure_generic_arguments(parser)
|
1228
|
-
args = parser.
|
1229
|
-
|
1268
|
+
args, rest = parser.parse_known_args()
|
1269
|
+
args.targets = args_to_uri(args.targets, args.loader, rest) if args.loader else args.targets
|
1230
1270
|
process_generic_arguments(args)
|
1231
1271
|
|
1232
1272
|
# For the shell tool we want -q to log slightly more then just CRITICAL
|