dissect.target 3.18.dev16__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/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.dev16.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/RECORD +93 -74
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
- {dissect.target-3.18.dev16.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.dev16.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
- {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,27 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import logging
|
5
|
+
from io import BytesIO
|
1
6
|
from pathlib import Path
|
7
|
+
from typing import Iterator
|
8
|
+
|
9
|
+
from dissect.target.helpers import hashutil
|
2
10
|
|
3
11
|
try:
|
4
12
|
import yara
|
13
|
+
|
14
|
+
HAS_YARA = True
|
15
|
+
|
5
16
|
except ImportError:
|
6
|
-
|
17
|
+
HAS_YARA = False
|
7
18
|
|
8
|
-
from dissect.target.exceptions import FileNotFoundError
|
19
|
+
from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
|
9
20
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
10
21
|
from dissect.target.plugin import Plugin, arg, export
|
11
22
|
|
23
|
+
log = logging.getLogger(__name__)
|
24
|
+
|
12
25
|
YaraMatchRecord = TargetRecordDescriptor(
|
13
26
|
"filesystem/yara/match",
|
14
27
|
[
|
@@ -16,48 +29,156 @@ YaraMatchRecord = TargetRecordDescriptor(
|
|
16
29
|
("digest", "digest"),
|
17
30
|
("string", "rule"),
|
18
31
|
("string[]", "tags"),
|
32
|
+
("string", "namespace"),
|
19
33
|
],
|
20
34
|
)
|
21
35
|
|
36
|
+
DEFAULT_MAX_SCAN_SIZE = 10 * 1024 * 1024
|
37
|
+
|
22
38
|
|
23
39
|
class YaraPlugin(Plugin):
|
24
40
|
"""Plugin to scan files against a local YARA rules file."""
|
25
41
|
|
26
|
-
DEFAULT_MAX_SIZE = 10 * 1024 * 1024
|
27
|
-
|
28
42
|
def check_compatible(self) -> None:
|
29
|
-
|
43
|
+
if not HAS_YARA:
|
44
|
+
raise UnsupportedPluginError("Please install 'yara-python' to use the yara plugin.")
|
30
45
|
|
31
|
-
@arg("
|
32
|
-
@arg("--
|
33
|
-
@arg("
|
46
|
+
@arg("-r", "--rules", required=True, nargs="*", help="path(s) to YARA rule file(s) or folder(s)")
|
47
|
+
@arg("-p", "--path", default="/", help="path on target(s) to recursively scan")
|
48
|
+
@arg("-m", "--max-size", type=int, default=DEFAULT_MAX_SCAN_SIZE, help="maximum file size in bytes to scan")
|
49
|
+
@arg("-c", "--check", default=False, action="store_true", help="check if every YARA rule is valid")
|
34
50
|
@export(record=YaraMatchRecord)
|
35
|
-
def yara(
|
36
|
-
|
51
|
+
def yara(
|
52
|
+
self,
|
53
|
+
rules: list[str | Path],
|
54
|
+
path: str = "/",
|
55
|
+
max_size: int = DEFAULT_MAX_SCAN_SIZE,
|
56
|
+
check: bool = False,
|
57
|
+
) -> Iterator[YaraMatchRecord]:
|
58
|
+
"""Scan files inside the target up to a given maximum size with YARA rule file(s).
|
59
|
+
|
60
|
+
Args:
|
61
|
+
rules: ``list`` of strings or ``Path`` objects pointing to rule files to use.
|
62
|
+
path: ``string`` of absolute target path to scan.
|
63
|
+
max_size: Files larger than this size will not be scanned.
|
64
|
+
check: Check if provided rules are valid, only compiles valid rules.
|
37
65
|
|
38
|
-
|
39
|
-
|
66
|
+
Returns:
|
67
|
+
Iterator yields ``YaraMatchRecord``.
|
40
68
|
"""
|
41
69
|
|
42
|
-
|
70
|
+
compiled_rules = process_rules(rules, check)
|
43
71
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
72
|
+
if not rules:
|
73
|
+
self.target.log.error("No working rules found in '%s'", ",".join(rules))
|
74
|
+
return
|
75
|
+
|
76
|
+
if hasattr(compiled_rules, "warnings") and (num_warns := len(compiled_rules.warnings)) > 0:
|
77
|
+
self.target.log.warning("YARA generated %s warnings while compiling rules", num_warns)
|
78
|
+
for warning in compiled_rules.warnings:
|
79
|
+
self.target.log.info(warning)
|
80
|
+
|
81
|
+
self.target.log.warning("Will not scan files larger than %s MB", max_size // 1024 // 1024)
|
82
|
+
|
83
|
+
for _, _, files in self.target.fs.walk_ext(path):
|
84
|
+
for file in files:
|
48
85
|
try:
|
49
|
-
if
|
86
|
+
if (file_size := file.stat().st_size) > max_size:
|
87
|
+
self.target.log.info("Not scanning file of %s MB: '%s'", (file_size // 1024 // 1024), file)
|
50
88
|
continue
|
51
89
|
|
52
|
-
|
90
|
+
buf = file.open().read()
|
91
|
+
for match in compiled_rules.match(data=buf):
|
53
92
|
yield YaraMatchRecord(
|
54
|
-
path=path,
|
55
|
-
digest=
|
93
|
+
path=self.target.fs.path(file.path),
|
94
|
+
digest=hashutil.common(BytesIO(buf)),
|
56
95
|
rule=match.rule,
|
57
96
|
tags=match.tags,
|
97
|
+
namespace=match.namespace,
|
58
98
|
_target=self.target,
|
59
99
|
)
|
100
|
+
|
60
101
|
except FileNotFoundError:
|
61
102
|
continue
|
62
|
-
except
|
63
|
-
self.target.log.
|
103
|
+
except RuntimeWarning as e:
|
104
|
+
self.target.log.warning("Runtime warning while scanning file '%s': %s", file, e)
|
105
|
+
except Exception as e:
|
106
|
+
self.target.log.error("Exception scanning file '%s'", file)
|
107
|
+
self.target.log.debug("", exc_info=e)
|
108
|
+
|
109
|
+
|
110
|
+
def process_rules(paths: list[str | Path], check: bool = False) -> yara.Rules | None:
|
111
|
+
"""Generate compiled YARA rules from the given path(s).
|
112
|
+
|
113
|
+
Provide path to one (compiled) YARA file or directory containing YARA files.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
paths: Path to file(s) or folder(s) containing YARA files.
|
117
|
+
check: Attempt to compile every rule file before appending to rules.
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
Compiled YARA rules or None.
|
121
|
+
"""
|
122
|
+
files = set()
|
123
|
+
compiled_rules = None
|
124
|
+
|
125
|
+
for rules_path in paths:
|
126
|
+
if isinstance(rules_path, str):
|
127
|
+
rules_path = Path(rules_path)
|
128
|
+
|
129
|
+
if not rules_path.exists():
|
130
|
+
log.warning("File %s does not exist!", rules_path)
|
131
|
+
continue
|
132
|
+
|
133
|
+
if rules_path.is_dir():
|
134
|
+
for file in rules_path.rglob("*"):
|
135
|
+
if not file.is_file():
|
136
|
+
continue
|
137
|
+
files.add(file)
|
138
|
+
else:
|
139
|
+
files.add(rules_path)
|
140
|
+
|
141
|
+
for file in set(files):
|
142
|
+
with file.open("rb") as fh:
|
143
|
+
magic = fh.read(4)
|
144
|
+
|
145
|
+
if magic == b"YARA":
|
146
|
+
if len(files) > 1:
|
147
|
+
log.error("Providing multiple compiled YARA files is not supported. Did not add %s", file)
|
148
|
+
continue
|
149
|
+
else:
|
150
|
+
log.info("Adding single compiled YARA file %s", file)
|
151
|
+
compiled_rules = compile_yara(file, is_compiled=True)
|
152
|
+
break
|
153
|
+
|
154
|
+
elif check and not is_valid_yara({"check_namespace": file}):
|
155
|
+
log.warning("File %s contains invalid rule(s)!", file)
|
156
|
+
files.remove(file)
|
157
|
+
continue
|
158
|
+
|
159
|
+
if files and not compiled_rules:
|
160
|
+
try:
|
161
|
+
compiled_rules = compile_yara({hashlib.md5(file.as_posix().encode()).hexdigest(): file for file in files})
|
162
|
+
except yara.Error as e:
|
163
|
+
log.error("Failed to compile YARA file(s): %s", e)
|
164
|
+
|
165
|
+
return compiled_rules
|
166
|
+
|
167
|
+
|
168
|
+
def compile_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> yara.Rules | None:
|
169
|
+
"""Compile or load the given YARA file(s) to rules."""
|
170
|
+
if is_compiled and isinstance(files, Path):
|
171
|
+
return yara.load(files.as_posix())
|
172
|
+
else:
|
173
|
+
return yara.compile(filepaths={ns: Path(path).as_posix() for ns, path in files.items()})
|
174
|
+
|
175
|
+
|
176
|
+
def is_valid_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> bool:
|
177
|
+
"""Determine if the given YARA file(s) compile without errors or warnings."""
|
178
|
+
try:
|
179
|
+
compile_yara(files, is_compiled)
|
180
|
+
return True
|
181
|
+
|
182
|
+
except (yara.SyntaxError, yara.WarningError, yara.Error) as e:
|
183
|
+
log.debug("Rule file(s) '%s' invalid: %s", files, e)
|
184
|
+
return False
|
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any, Iterator, Union
|
4
|
+
|
5
|
+
from flow.record.fieldtypes.net import IPAddress, IPNetwork
|
6
|
+
|
7
|
+
from dissect.target.helpers.record import (
|
8
|
+
MacInterfaceRecord,
|
9
|
+
UnixInterfaceRecord,
|
10
|
+
WindowsInterfaceRecord,
|
11
|
+
)
|
12
|
+
from dissect.target.plugin import Plugin, export, internal
|
13
|
+
from dissect.target.target import Target
|
14
|
+
|
15
|
+
InterfaceRecord = Union[UnixInterfaceRecord, WindowsInterfaceRecord, MacInterfaceRecord]
|
16
|
+
|
17
|
+
|
18
|
+
class NetworkPlugin(Plugin):
|
19
|
+
__namespace__ = "network"
|
20
|
+
|
21
|
+
def __init__(self, target: Target):
|
22
|
+
super().__init__(target)
|
23
|
+
self._interface_list: list[InterfaceRecord] | None = None
|
24
|
+
|
25
|
+
def check_compatible(self) -> None:
|
26
|
+
pass
|
27
|
+
|
28
|
+
def _interfaces(self) -> Iterator[InterfaceRecord]:
|
29
|
+
yield from ()
|
30
|
+
|
31
|
+
def _get_record_type(self, field_name: str) -> Iterator[Any]:
|
32
|
+
for record in self.interfaces():
|
33
|
+
if (output := getattr(record, field_name, None)) is None:
|
34
|
+
continue
|
35
|
+
|
36
|
+
if isinstance(output, list):
|
37
|
+
yield from output
|
38
|
+
else:
|
39
|
+
yield output
|
40
|
+
|
41
|
+
@export(record=InterfaceRecord)
|
42
|
+
def interfaces(self) -> Iterator[InterfaceRecord]:
|
43
|
+
# Only search for the interfaces once
|
44
|
+
if self._interface_list is None:
|
45
|
+
self._interface_list = list(self._interfaces())
|
46
|
+
|
47
|
+
yield from self._interface_list
|
48
|
+
|
49
|
+
@export
|
50
|
+
def ips(self) -> list[IPAddress]:
|
51
|
+
return list(self._get_record_type("ip"))
|
52
|
+
|
53
|
+
@export
|
54
|
+
def gateways(self) -> list[IPAddress]:
|
55
|
+
return list(self._get_record_type("gateway"))
|
56
|
+
|
57
|
+
@export
|
58
|
+
def macs(self) -> list[str]:
|
59
|
+
return list(self._get_record_type("mac"))
|
60
|
+
|
61
|
+
@export
|
62
|
+
def dns(self) -> list[str]:
|
63
|
+
return list(self._get_record_type("dns"))
|
64
|
+
|
65
|
+
@internal
|
66
|
+
def with_ip(self, ip_addr: str) -> Iterator[InterfaceRecord]:
|
67
|
+
for interface in self.interfaces():
|
68
|
+
if ip_addr in interface.ip:
|
69
|
+
yield interface
|
70
|
+
|
71
|
+
@internal
|
72
|
+
def with_mac(self, mac: str) -> Iterator[InterfaceRecord]:
|
73
|
+
for interface in self.interfaces():
|
74
|
+
if interface.mac == mac:
|
75
|
+
yield interface
|
76
|
+
|
77
|
+
@internal
|
78
|
+
def in_cidr(self, cidr: str) -> Iterator[InterfaceRecord]:
|
79
|
+
cidr = IPNetwork(cidr)
|
80
|
+
for interface in self.interfaces():
|
81
|
+
if any(ip_addr in cidr for ip_addr in interface.ip):
|
82
|
+
yield interface
|
@@ -1,5 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from functools import lru_cache
|
2
|
-
from typing import
|
4
|
+
from typing import Iterator, NamedTuple, Union
|
3
5
|
|
4
6
|
from dissect.target import Target
|
5
7
|
from dissect.target.exceptions import UnsupportedPluginError
|
@@ -7,10 +9,12 @@ from dissect.target.helpers.fsutil import TargetPath
|
|
7
9
|
from dissect.target.helpers.record import UnixUserRecord, WindowsUserRecord
|
8
10
|
from dissect.target.plugin import InternalPlugin
|
9
11
|
|
12
|
+
UserRecord = Union[UnixUserRecord, WindowsUserRecord]
|
13
|
+
|
10
14
|
|
11
15
|
class UserDetails(NamedTuple):
|
12
|
-
user:
|
13
|
-
home_path:
|
16
|
+
user: UserRecord
|
17
|
+
home_path: TargetPath | None
|
14
18
|
|
15
19
|
|
16
20
|
class UsersPlugin(InternalPlugin):
|
@@ -28,11 +32,11 @@ class UsersPlugin(InternalPlugin):
|
|
28
32
|
|
29
33
|
def find(
|
30
34
|
self,
|
31
|
-
sid:
|
32
|
-
uid:
|
33
|
-
username:
|
35
|
+
sid: str | None = None,
|
36
|
+
uid: str | None = None,
|
37
|
+
username: str | None = None,
|
34
38
|
force_case_sensitive: bool = False,
|
35
|
-
) ->
|
39
|
+
) -> UserDetails | None:
|
36
40
|
"""Find User record matching provided sid, uid or username and return UserDetails object"""
|
37
41
|
if all(map(lambda x: x is None, [sid, uid, username])):
|
38
42
|
raise ValueError("Either sid or uid or username is expected")
|
@@ -52,7 +56,7 @@ class UsersPlugin(InternalPlugin):
|
|
52
56
|
):
|
53
57
|
return self.get(user)
|
54
58
|
|
55
|
-
def get(self, user:
|
59
|
+
def get(self, user: UserRecord) -> UserDetails:
|
56
60
|
"""Return additional details about the user"""
|
57
61
|
# Resolving the user home can not use the user's environment variables,
|
58
62
|
# as those depend on the user's home to be known first. So we resolve
|
@@ -60,12 +64,12 @@ class UsersPlugin(InternalPlugin):
|
|
60
64
|
home_path = self.target.fs.path(self.target.resolve(str(user.home))) if user.home else None
|
61
65
|
return UserDetails(user=user, home_path=home_path)
|
62
66
|
|
63
|
-
def all(self) ->
|
67
|
+
def all(self) -> Iterator[UserDetails]:
|
64
68
|
"""Return UserDetails objects for all users found"""
|
65
69
|
for user in self.target.users():
|
66
70
|
yield self.get(user)
|
67
71
|
|
68
|
-
def all_with_home(self) ->
|
72
|
+
def all_with_home(self) -> Iterator[UserDetails]:
|
69
73
|
"""Return UserDetails objects for users that have existing directory set as home directory"""
|
70
74
|
for user in self.target.users():
|
71
75
|
if user.home:
|
@@ -40,12 +40,18 @@ class UnixPlugin(OSPlugin):
|
|
40
40
|
@export(record=UnixUserRecord)
|
41
41
|
@arg("--sessions", action="store_true", help="Parse syslog for recent user sessions")
|
42
42
|
def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
|
43
|
-
"""
|
43
|
+
"""Yield unix user records from passwd files or syslog session logins.
|
44
|
+
|
45
|
+
Resources:
|
46
|
+
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
|
47
|
+
"""
|
48
|
+
|
49
|
+
PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]
|
44
50
|
|
45
51
|
seen_users = set()
|
46
52
|
|
47
53
|
# Yield users found in passwd files.
|
48
|
-
for passwd_file in
|
54
|
+
for passwd_file in PASSWD_FILES:
|
49
55
|
if (path := self.target.fs.path(passwd_file)).exists():
|
50
56
|
for line in path.open("rt"):
|
51
57
|
line = line.strip()
|
@@ -53,7 +59,12 @@ class UnixPlugin(OSPlugin):
|
|
53
59
|
continue
|
54
60
|
|
55
61
|
pwent = dict(enumerate(line.split(":")))
|
56
|
-
|
62
|
+
|
63
|
+
current_user = (pwent.get(0), pwent.get(5), pwent.get(6))
|
64
|
+
if current_user in seen_users:
|
65
|
+
continue
|
66
|
+
|
67
|
+
seen_users.add(current_user)
|
57
68
|
yield UnixUserRecord(
|
58
69
|
name=pwent.get(0),
|
59
70
|
passwd=pwent.get(1),
|
@@ -202,11 +213,13 @@ class UnixPlugin(OSPlugin):
|
|
202
213
|
fs_id = None
|
203
214
|
fs_subvol = None
|
204
215
|
fs_subvolid = None
|
205
|
-
fs_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
|
206
216
|
fs_last_mount = None
|
217
|
+
fs_volume_name = None
|
218
|
+
vol_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
|
207
219
|
|
208
220
|
if fs.__type__ == "xfs":
|
209
221
|
fs_id = fs.xfs.uuid
|
222
|
+
fs_volume_name = fs.xfs.name
|
210
223
|
elif fs.__type__ == "ext":
|
211
224
|
fs_id = fs.extfs.uuid
|
212
225
|
fs_last_mount = fs.extfs.last_mount
|
@@ -224,8 +237,9 @@ class UnixPlugin(OSPlugin):
|
|
224
237
|
|
225
238
|
if (
|
226
239
|
(fs_id and (fs_id == dev_id and (subvol == fs_subvol or subvolid == fs_subvolid)))
|
227
|
-
or (fs_volume_name and (fs_volume_name == volume_name))
|
228
240
|
or (fs_last_mount and (fs_last_mount == mount_point))
|
241
|
+
or (fs_volume_name and (fs_volume_name == volume_name))
|
242
|
+
or (vol_volume_name and (vol_volume_name == volume_name))
|
229
243
|
):
|
230
244
|
self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
|
231
245
|
self.target.fs.mount(mount_point, fs)
|
@@ -1,7 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Optional
|
4
|
-
|
5
3
|
from dissect.target.filesystem import Filesystem
|
6
4
|
from dissect.target.plugin import export
|
7
5
|
from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
|
@@ -14,13 +12,13 @@ class FreeBsdPlugin(BsdPlugin):
|
|
14
12
|
self._os_release = self._parse_os_release("/bin/freebsd-version*")
|
15
13
|
|
16
14
|
@classmethod
|
17
|
-
def detect(cls, target: Target) ->
|
15
|
+
def detect(cls, target: Target) -> Filesystem | None:
|
18
16
|
for fs in target.filesystems:
|
19
|
-
if fs.exists("/net")
|
17
|
+
if fs.exists("/net") and (fs.exists("/.sujournal") or fs.exists("/entropy")):
|
20
18
|
return fs
|
21
19
|
|
22
20
|
return None
|
23
21
|
|
24
22
|
@export(property=True)
|
25
|
-
def version(self) ->
|
23
|
+
def version(self) -> str | None:
|
26
24
|
return self._os_release.get("USERLAND_VERSION")
|
@@ -8,7 +8,7 @@ import subprocess
|
|
8
8
|
from configparser import ConfigParser
|
9
9
|
from configparser import Error as ConfigParserError
|
10
10
|
from io import BytesIO
|
11
|
-
from typing import Any, BinaryIO, Iterator,
|
11
|
+
from typing import Any, BinaryIO, Iterator, TextIO
|
12
12
|
|
13
13
|
from defusedxml import ElementTree
|
14
14
|
from dissect.hypervisor.util import vmtar
|
@@ -17,9 +17,14 @@ from dissect.sql import sqlite3
|
|
17
17
|
from dissect.target.helpers.fsutil import TargetPath
|
18
18
|
|
19
19
|
try:
|
20
|
-
from dissect.hypervisor.util.envelope import
|
20
|
+
from dissect.hypervisor.util.envelope import (
|
21
|
+
HAS_PYCRYPTODOME,
|
22
|
+
HAS_PYSTANDALONE,
|
23
|
+
Envelope,
|
24
|
+
KeyStore,
|
25
|
+
)
|
21
26
|
|
22
|
-
HAS_ENVELOPE =
|
27
|
+
HAS_ENVELOPE = HAS_PYCRYPTODOME or HAS_PYSTANDALONE
|
23
28
|
except ImportError:
|
24
29
|
HAS_ENVELOPE = False
|
25
30
|
|
@@ -68,9 +73,10 @@ class ESXiPlugin(UnixPlugin):
|
|
68
73
|
if configstore.exists():
|
69
74
|
self._configstore = parse_config_store(configstore.open())
|
70
75
|
|
71
|
-
def _cfg(self, path: str) ->
|
76
|
+
def _cfg(self, path: str) -> str | None:
|
72
77
|
if not self._config:
|
73
|
-
|
78
|
+
self.target.log.warning("No ESXi config!")
|
79
|
+
return None
|
74
80
|
|
75
81
|
value_name = path.strip("/").split("/")[-1]
|
76
82
|
obj = _traverse(path, self._config)
|
@@ -81,7 +87,7 @@ class ESXiPlugin(UnixPlugin):
|
|
81
87
|
return obj.get(value_name) if obj else None
|
82
88
|
|
83
89
|
@classmethod
|
84
|
-
def detect(cls, target: Target) ->
|
90
|
+
def detect(cls, target: Target) -> Filesystem | None:
|
85
91
|
bootbanks = [
|
86
92
|
fs for fs in target.filesystems if fs.path("boot.cfg").exists() and list(fs.path("/").glob("*.v00"))
|
87
93
|
]
|
@@ -95,13 +101,13 @@ class ESXiPlugin(UnixPlugin):
|
|
95
101
|
def create(cls, target: Target, sysvol: Filesystem) -> ESXiPlugin:
|
96
102
|
cfg = parse_boot_cfg(sysvol.path("boot.cfg").open("rt"))
|
97
103
|
|
104
|
+
# Mount all the visor tars in individual filesystem layers
|
105
|
+
_mount_modules(target, sysvol, cfg)
|
106
|
+
|
98
107
|
# Create a root layer for the "local state" filesystem
|
99
108
|
# This stores persistent configuration data
|
100
109
|
local_layer = target.fs.append_layer()
|
101
110
|
|
102
|
-
# Mount all the visor tars in individual filesystem layers
|
103
|
-
_mount_modules(target, sysvol, cfg)
|
104
|
-
|
105
111
|
# Mount the local.tgz to the local state layer
|
106
112
|
_mount_local(target, local_layer)
|
107
113
|
|
@@ -122,7 +128,7 @@ class ESXiPlugin(UnixPlugin):
|
|
122
128
|
return "localhost"
|
123
129
|
|
124
130
|
@export(property=True)
|
125
|
-
def domain(self) ->
|
131
|
+
def domain(self) -> str | None:
|
126
132
|
if hostname := self._cfg("/adv/Misc/HostName"):
|
127
133
|
return hostname.partition(".")[2]
|
128
134
|
|
@@ -140,7 +146,7 @@ class ESXiPlugin(UnixPlugin):
|
|
140
146
|
return list(result)
|
141
147
|
|
142
148
|
@export(property=True)
|
143
|
-
def version(self) ->
|
149
|
+
def version(self) -> str | None:
|
144
150
|
boot_cfg = self.target.fs.path("/bootbank/boot.cfg")
|
145
151
|
if not boot_cfg.exists():
|
146
152
|
return None
|
@@ -181,11 +187,11 @@ class ESXiPlugin(UnixPlugin):
|
|
181
187
|
return self._configstore
|
182
188
|
|
183
189
|
@export(property=True)
|
184
|
-
def os(self):
|
190
|
+
def os(self) -> str:
|
185
191
|
return OperatingSystem.ESXI.value
|
186
192
|
|
187
193
|
|
188
|
-
def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
|
194
|
+
def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
|
189
195
|
modules = [m.strip() for m in cfg["modules"].split("---")]
|
190
196
|
|
191
197
|
for module in modules:
|
@@ -212,20 +218,22 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
|
|
212
218
|
target.fs.append_layer().mount("/", tfs)
|
213
219
|
|
214
220
|
|
215
|
-
def _mount_local(target: Target, local_layer: VirtualFilesystem):
|
221
|
+
def _mount_local(target: Target, local_layer: VirtualFilesystem) -> None:
|
216
222
|
local_tgz = target.fs.path("local.tgz")
|
223
|
+
local_tgz_ve = target.fs.path("local.tgz.ve")
|
217
224
|
local_fs = None
|
218
225
|
|
219
226
|
if local_tgz.exists():
|
220
227
|
local_fs = tar.TarFilesystem(local_tgz.open())
|
221
|
-
|
222
|
-
local_tgz_ve = target.fs.path("local.tgz.ve")
|
228
|
+
elif local_tgz_ve.exists():
|
223
229
|
# In the case "encryption.info" does not exist, but ".#encryption.info" does
|
224
230
|
encryption_info = next(target.fs.path("/").glob("*encryption.info"), None)
|
225
231
|
if not local_tgz_ve.exists() or not encryption_info.exists():
|
226
232
|
raise ValueError("Unable to find valid configuration archive")
|
227
233
|
|
228
234
|
local_fs = _create_local_fs(target, local_tgz_ve, encryption_info)
|
235
|
+
else:
|
236
|
+
target.log.warning("No local.tgz or local.tgz.ve found, skipping local state")
|
229
237
|
|
230
238
|
if local_fs:
|
231
239
|
local_layer.mount("/", local_fs)
|
@@ -239,7 +247,7 @@ def _decrypt_envelope(local_tgz_ve: TargetPath, encryption_info: TargetPath) ->
|
|
239
247
|
return local_tgz
|
240
248
|
|
241
249
|
|
242
|
-
def _decrypt_crypto_util(local_tgz_ve: TargetPath) ->
|
250
|
+
def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> BytesIO | None:
|
243
251
|
"""Decrypt ``local.tgz.ve`` using ESXi ``crypto-util``.
|
244
252
|
|
245
253
|
We write to stdout, but this results in ``crypto-util`` exiting with a non-zero return code
|
@@ -258,9 +266,7 @@ def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> Optional[BytesIO]:
|
|
258
266
|
return BytesIO(result.stdout)
|
259
267
|
|
260
268
|
|
261
|
-
def _create_local_fs(
|
262
|
-
target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath
|
263
|
-
) -> Optional[tar.TarFilesystem]:
|
269
|
+
def _create_local_fs(target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath) -> tar.TarFilesystem | None:
|
264
270
|
local_tgz = None
|
265
271
|
|
266
272
|
if HAS_ENVELOPE:
|
@@ -286,7 +292,7 @@ def _create_local_fs(
|
|
286
292
|
return tar.TarFilesystem(local_tgz)
|
287
293
|
|
288
294
|
|
289
|
-
def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
|
295
|
+
def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
|
290
296
|
version = cfg["build"]
|
291
297
|
|
292
298
|
osdata_fs = None
|
@@ -365,7 +371,7 @@ def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
|
|
365
371
|
target.fs.symlink(f"/vmfs/volumes/LOCKER-{locker_fs.vmfs.uuid}", "/locker")
|
366
372
|
|
367
373
|
|
368
|
-
def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin):
|
374
|
+
def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin) -> None:
|
369
375
|
version = cfg["build"]
|
370
376
|
|
371
377
|
# Don't really know how ESXi does this, but let's just take a shortcut for now
|
@@ -435,7 +441,7 @@ def parse_esx_conf(fh: TextIO) -> dict[str, Any]:
|
|
435
441
|
return config
|
436
442
|
|
437
443
|
|
438
|
-
def _traverse(path: str, obj: dict[str, Any], create: bool = False):
|
444
|
+
def _traverse(path: str, obj: dict[str, Any], create: bool = False) -> dict[str, Any] | None:
|
439
445
|
parts = path.strip("/").split("/")
|
440
446
|
path_parts = parts[:-1]
|
441
447
|
for part in path_parts:
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import fnmatch
|
2
|
-
import logging
|
3
2
|
import re
|
4
3
|
from pathlib import Path
|
5
4
|
from typing import Iterator
|
@@ -22,8 +21,6 @@ UnixConfigTreeRecord = TargetRecordDescriptor(
|
|
22
21
|
],
|
23
22
|
)
|
24
23
|
|
25
|
-
log = logging.getLogger(__name__)
|
26
|
-
|
27
24
|
|
28
25
|
class EtcTree(ConfigurationTreePlugin):
|
29
26
|
__namespace__ = "etc"
|
@@ -65,13 +62,13 @@ class EtcTree(ConfigurationTreePlugin):
|
|
65
62
|
|
66
63
|
@export(record=UnixConfigTreeRecord)
|
67
64
|
@arg("--glob", dest="pattern", required=False, default="*", type=str, help="Glob-style pattern to search for")
|
68
|
-
|
69
|
-
|
65
|
+
@arg("--root", dest="root", required=False, default="/", type=str, help="Path to use as root for search")
|
66
|
+
def etc(self, pattern: str, root: str) -> Iterator[UnixConfigTreeRecord]:
|
67
|
+
for entry, subs, items in self.config_fs.walk(root):
|
70
68
|
for item in items:
|
71
69
|
try:
|
72
70
|
config_object = self.get(str(Path(entry) / Path(item)))
|
73
|
-
|
74
|
-
yield from self._sub(config_object, Path(entry) / Path(item), pattern)
|
71
|
+
yield from self._sub(config_object, Path(entry) / Path(item), pattern)
|
75
72
|
except Exception:
|
76
|
-
log.warning("Could not open configuration item: %s", item)
|
73
|
+
self.target.log.warning("Could not open configuration item: %s", item)
|
77
74
|
pass
|
@@ -8,7 +8,7 @@ from dissect.target.exceptions import UnsupportedPluginError
|
|
8
8
|
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
9
9
|
from dissect.target.helpers.fsutil import TargetPath
|
10
10
|
from dissect.target.helpers.record import UnixUserRecord, create_extended_descriptor
|
11
|
-
from dissect.target.plugin import Plugin, export, internal
|
11
|
+
from dissect.target.plugin import Plugin, alias, export, internal
|
12
12
|
|
13
13
|
CommandHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
14
14
|
"unix/history",
|
@@ -36,6 +36,7 @@ class CommandHistoryPlugin(Plugin):
|
|
36
36
|
("sqlite", ".sqlite_history"),
|
37
37
|
("zsh", ".zsh_history"),
|
38
38
|
("ash", ".ash_history"),
|
39
|
+
("dissect", ".dissect_history"), # wow so meta
|
39
40
|
)
|
40
41
|
|
41
42
|
def __init__(self, target: Target):
|
@@ -56,12 +57,7 @@ class CommandHistoryPlugin(Plugin):
|
|
56
57
|
history_files.append((shell, history_path, user_details.user))
|
57
58
|
return history_files
|
58
59
|
|
59
|
-
@
|
60
|
-
def bashhistory(self):
|
61
|
-
"""Deprecated, use commandhistory function."""
|
62
|
-
self.target.log.warn("Function 'bashhistory' is deprecated, use the 'commandhistory' function instead.")
|
63
|
-
return self.commandhistory()
|
64
|
-
|
60
|
+
@alias("bashhistory")
|
65
61
|
@export(record=CommandHistoryRecord)
|
66
62
|
def commandhistory(self):
|
67
63
|
"""Return shell history for all users.
|