dissect.target 3.14.dev8__py3-none-any.whl → 3.14.dev10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/filesystem.py +87 -7
- dissect/target/filesystems/btrfs.py +180 -0
- dissect/target/helpers/utils.py +14 -3
- dissect/target/plugins/apps/av/symantec.py +3 -4
- dissect/target/plugins/os/unix/_os.py +27 -18
- dissect/target/target.py +29 -25
- dissect/target/tools/mount.py +2 -12
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/METADATA +2 -1
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/RECORD +14 -13
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/LICENSE +0 -0
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/WHEEL +0 -0
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.14.dev8.dist-info → dissect.target-3.14.dev10.dist-info}/top_level.txt +0 -0
dissect/target/filesystem.py
CHANGED
@@ -45,10 +45,12 @@ class Filesystem:
|
|
45
45
|
# This has the added benefit of having a readily available "pretty name" for each implementation
|
46
46
|
__type__: str = None
|
47
47
|
"""A short string identifying the type of filesystem."""
|
48
|
+
__multi_volume__: bool = False
|
49
|
+
"""Whether this filesystem supports multiple volumes (disks)."""
|
48
50
|
|
49
51
|
def __init__(
|
50
52
|
self,
|
51
|
-
volume: Optional[BinaryIO] = None,
|
53
|
+
volume: Optional[Union[BinaryIO, list[BinaryIO]]] = None,
|
52
54
|
alt_separator: str = "",
|
53
55
|
case_sensitive: bool = True,
|
54
56
|
) -> None:
|
@@ -56,7 +58,7 @@ class Filesystem:
|
|
56
58
|
|
57
59
|
Args:
|
58
60
|
volume: A volume or other file-like object associated with the filesystem.
|
59
|
-
case_sensitive: Defines if the paths in the
|
61
|
+
case_sensitive: Defines if the paths in the filesystem are case sensitive or not.
|
60
62
|
alt_separator: The alternative separator used to distingish between directories in a path.
|
61
63
|
|
62
64
|
Raises:
|
@@ -82,7 +84,7 @@ class Filesystem:
|
|
82
84
|
return cls.__type__
|
83
85
|
|
84
86
|
def path(self, *args) -> fsutil.TargetPath:
|
85
|
-
"""
|
87
|
+
"""Instantiate a new path-like object on this filesystem."""
|
86
88
|
return fsutil.TargetPath(self, *args)
|
87
89
|
|
88
90
|
@classmethod
|
@@ -125,6 +127,52 @@ class Filesystem:
|
|
125
127
|
"""
|
126
128
|
raise NotImplementedError()
|
127
129
|
|
130
|
+
@classmethod
|
131
|
+
def detect_id(cls, fh: BinaryIO) -> Optional[bytes]:
|
132
|
+
"""Return a filesystem set identifier.
|
133
|
+
|
134
|
+
Only used in filesystems that support multiple volumes (disks) to find all volumes
|
135
|
+
belonging to a single filesystem.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
fh: A file-like object, usually a disk or partition.
|
139
|
+
"""
|
140
|
+
if not cls.__multi_volume__:
|
141
|
+
return None
|
142
|
+
|
143
|
+
offset = fh.tell()
|
144
|
+
try:
|
145
|
+
fh.seek(0)
|
146
|
+
return cls._detect_id(fh)
|
147
|
+
except NotImplementedError:
|
148
|
+
raise
|
149
|
+
except Exception as e:
|
150
|
+
log.warning("Failed to detect ID on %s filesystem", cls.__fstype__)
|
151
|
+
log.debug("", exc_info=e)
|
152
|
+
finally:
|
153
|
+
fh.seek(offset)
|
154
|
+
|
155
|
+
return None
|
156
|
+
|
157
|
+
@staticmethod
|
158
|
+
def _detect_id(fh: BinaryIO) -> Optional[bytes]:
|
159
|
+
"""Return a filesystem set identifier.
|
160
|
+
|
161
|
+
This method should be implemented by subclasses of filesystems that support multiple volumes (disks).
|
162
|
+
The position of ``fh`` is guaranteed to be ``0``.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
fh: A file-like object, usually a disk or partition.
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
An identifier that can be used to combine the given ``fh`` with others beloning to the same set.
|
169
|
+
"""
|
170
|
+
raise NotImplementedError()
|
171
|
+
|
172
|
+
def iter_subfs(self) -> Iterator[Filesystem]:
|
173
|
+
"""Yield possible sub-filesystems."""
|
174
|
+
yield from ()
|
175
|
+
|
128
176
|
def get(self, path: str) -> FilesystemEntry:
|
129
177
|
"""Retrieve a :class:`FilesystemEntry` from the filesystem.
|
130
178
|
|
@@ -1461,6 +1509,18 @@ def register(module: str, class_name: str, internal: bool = True) -> None:
|
|
1461
1509
|
FILESYSTEMS.append(getattr(import_lazy(module), class_name))
|
1462
1510
|
|
1463
1511
|
|
1512
|
+
def is_multi_volume_filesystem(fh: BinaryIO) -> bool:
|
1513
|
+
for filesystem in FILESYSTEMS:
|
1514
|
+
try:
|
1515
|
+
if filesystem.__multi_volume__ and filesystem.detect(fh):
|
1516
|
+
return True
|
1517
|
+
except ImportError as e:
|
1518
|
+
log.info("Failed to import %s", filesystem)
|
1519
|
+
log.debug("", exc_info=e)
|
1520
|
+
|
1521
|
+
return False
|
1522
|
+
|
1523
|
+
|
1464
1524
|
def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
|
1465
1525
|
offset = fh.tell()
|
1466
1526
|
fh.seek(0)
|
@@ -1469,10 +1529,7 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
|
|
1469
1529
|
for filesystem in FILESYSTEMS:
|
1470
1530
|
try:
|
1471
1531
|
if filesystem.detect(fh):
|
1472
|
-
|
1473
|
-
instance.volume = fh
|
1474
|
-
|
1475
|
-
return instance
|
1532
|
+
return filesystem(fh, *args, **kwargs)
|
1476
1533
|
except ImportError as e:
|
1477
1534
|
log.info("Failed to import %s", filesystem)
|
1478
1535
|
log.debug("", exc_info=e)
|
@@ -1482,12 +1539,35 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
|
|
1482
1539
|
raise FilesystemError(f"Failed to open filesystem for {fh}")
|
1483
1540
|
|
1484
1541
|
|
1542
|
+
def open_multi_volume(fhs: list[BinaryIO], *args, **kwargs) -> Filesystem:
|
1543
|
+
for filesystem in FILESYSTEMS:
|
1544
|
+
try:
|
1545
|
+
if not filesystem.__multi_volume__:
|
1546
|
+
continue
|
1547
|
+
|
1548
|
+
volumes = defaultdict(list)
|
1549
|
+
for fh in fhs:
|
1550
|
+
if not filesystem.detect(fh):
|
1551
|
+
continue
|
1552
|
+
|
1553
|
+
identifier = filesystem.detect_id(fh)
|
1554
|
+
volumes[identifier].append(fh)
|
1555
|
+
|
1556
|
+
for vols in volumes.values():
|
1557
|
+
yield filesystem(vols, *args, **kwargs)
|
1558
|
+
|
1559
|
+
except ImportError as e:
|
1560
|
+
log.info("Failed to import %s", filesystem)
|
1561
|
+
log.debug("", exc_info=e)
|
1562
|
+
|
1563
|
+
|
1485
1564
|
register("ntfs", "NtfsFilesystem")
|
1486
1565
|
register("extfs", "ExtFilesystem")
|
1487
1566
|
register("xfs", "XfsFilesystem")
|
1488
1567
|
register("fat", "FatFilesystem")
|
1489
1568
|
register("ffs", "FfsFilesystem")
|
1490
1569
|
register("vmfs", "VmfsFilesystem")
|
1570
|
+
register("btrfs", "BtrfsFilesystem")
|
1491
1571
|
register("exfat", "ExfatFilesystem")
|
1492
1572
|
register("squashfs", "SquashFSFilesystem")
|
1493
1573
|
register("zip", "ZipFilesystem")
|
@@ -0,0 +1,180 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import BinaryIO, Iterator, Optional, Union
|
4
|
+
|
5
|
+
import dissect.btrfs as btrfs
|
6
|
+
from dissect.btrfs.c_btrfs import c_btrfs
|
7
|
+
|
8
|
+
from dissect.target.exceptions import (
|
9
|
+
FileNotFoundError,
|
10
|
+
FilesystemError,
|
11
|
+
IsADirectoryError,
|
12
|
+
NotADirectoryError,
|
13
|
+
NotASymlinkError,
|
14
|
+
)
|
15
|
+
from dissect.target.filesystem import Filesystem, FilesystemEntry
|
16
|
+
from dissect.target.helpers import fsutil
|
17
|
+
|
18
|
+
|
19
|
+
class BtrfsFilesystem(Filesystem):
|
20
|
+
__fstype__ = "btrfs"
|
21
|
+
__multi_volume__ = True
|
22
|
+
|
23
|
+
def __init__(self, fh: Union[BinaryIO, list[BinaryIO]], *args, **kwargs):
|
24
|
+
super().__init__(fh, *args, **kwargs)
|
25
|
+
self.btrfs = btrfs.Btrfs(fh)
|
26
|
+
self.subfs = self.open_subvolume()
|
27
|
+
self.subvolume = self.subfs.subvolume
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def _detect(fh: BinaryIO) -> bool:
|
31
|
+
fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET)
|
32
|
+
block = fh.read(4096)
|
33
|
+
magic = int.from_bytes(block[64:72], "little")
|
34
|
+
|
35
|
+
return magic == c_btrfs.BTRFS_MAGIC
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def _detect_id(fh: BinaryIO) -> Optional[bytes]:
|
39
|
+
# First field is csum, followed by fsid
|
40
|
+
fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET + c_btrfs.BTRFS_CSUM_SIZE)
|
41
|
+
return fh.read(c_btrfs.BTRFS_FSID_SIZE)
|
42
|
+
|
43
|
+
def iter_subfs(self) -> Iterator[BtrfsSubvolumeFilesystem]:
|
44
|
+
for subvol in self.btrfs.subvolumes():
|
45
|
+
if subvol.objectid == self.subfs.subvolume.objectid:
|
46
|
+
# Skip the default volume as it's already opened by the main filesystem
|
47
|
+
continue
|
48
|
+
yield self.open_subvolume(subvolid=subvol.objectid)
|
49
|
+
|
50
|
+
def open_subvolume(self, subvol: Optional[str] = None, subvolid: Optional[int] = None) -> BtrfsSubvolumeFilesystem:
|
51
|
+
return BtrfsSubvolumeFilesystem(self, subvol, subvolid)
|
52
|
+
|
53
|
+
def get(self, path: str) -> FilesystemEntry:
|
54
|
+
return self.subfs.get(path)
|
55
|
+
|
56
|
+
|
57
|
+
class BtrfsSubvolumeFilesystem(Filesystem):
|
58
|
+
__fstype__ = "btrfs"
|
59
|
+
|
60
|
+
def __init__(self, fs: BtrfsFilesystem, subvol: Optional[str] = None, subvolid: Optional[int] = None):
|
61
|
+
super().__init__(fs.volume, alt_separator=fs.alt_separator, case_sensitive=fs.case_sensitive)
|
62
|
+
if subvol is not None and subvolid is not None:
|
63
|
+
raise ValueError("Only one of subvol or subvolid is allowed")
|
64
|
+
|
65
|
+
self.fs = fs
|
66
|
+
self.btrfs = fs.btrfs
|
67
|
+
if subvol:
|
68
|
+
self.subvolume = self.btrfs.find_subvolume(subvol)
|
69
|
+
elif subvolid:
|
70
|
+
self.subvolume = self.btrfs.open_subvolume(subvolid)
|
71
|
+
else:
|
72
|
+
self.subvolume = self.btrfs.default_subvolume
|
73
|
+
|
74
|
+
def get(self, path: str) -> FilesystemEntry:
|
75
|
+
return BtrfsFilesystemEntry(self, path, self._get_node(path))
|
76
|
+
|
77
|
+
def _get_node(self, path: str, node: Optional[btrfs.INode] = None) -> btrfs.INode:
|
78
|
+
try:
|
79
|
+
return self.subvolume.get(path, node)
|
80
|
+
except btrfs.FileNotFoundError as e:
|
81
|
+
raise FileNotFoundError(path, cause=e)
|
82
|
+
except btrfs.NotADirectoryError as e:
|
83
|
+
raise NotADirectoryError(path, cause=e)
|
84
|
+
except btrfs.NotASymlinkError as e:
|
85
|
+
raise NotASymlinkError(path, cause=e)
|
86
|
+
except btrfs.Error as e:
|
87
|
+
raise FileNotFoundError(path, cause=e)
|
88
|
+
|
89
|
+
|
90
|
+
class BtrfsFilesystemEntry(FilesystemEntry):
|
91
|
+
fs: BtrfsFilesystem
|
92
|
+
entry: btrfs.INode
|
93
|
+
|
94
|
+
def get(self, path: str) -> FilesystemEntry:
|
95
|
+
entry_path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
|
96
|
+
entry = self.fs._get_node(path, self.entry)
|
97
|
+
return BtrfsFilesystemEntry(self.fs, entry_path, entry)
|
98
|
+
|
99
|
+
def open(self) -> BinaryIO:
|
100
|
+
if self.is_dir():
|
101
|
+
raise IsADirectoryError(self.path)
|
102
|
+
return self._resolve().entry.open()
|
103
|
+
|
104
|
+
def _iterdir(self) -> Iterator[btrfs.INode]:
|
105
|
+
if not self.is_dir():
|
106
|
+
raise NotADirectoryError(self.path)
|
107
|
+
|
108
|
+
if self.is_symlink():
|
109
|
+
for entry in self.readlink_ext().iterdir():
|
110
|
+
yield entry
|
111
|
+
else:
|
112
|
+
for name, entry in self.entry.iterdir():
|
113
|
+
if name in (".", ".."):
|
114
|
+
continue
|
115
|
+
|
116
|
+
yield name, entry
|
117
|
+
|
118
|
+
def iterdir(self) -> Iterator[str]:
|
119
|
+
for name, _ in self._iterdir():
|
120
|
+
yield name
|
121
|
+
|
122
|
+
def scandir(self) -> Iterator[FilesystemEntry]:
|
123
|
+
for name, entry in self._iterdir():
|
124
|
+
entry_path = fsutil.join(self.path, name, alt_separator=self.fs.alt_separator)
|
125
|
+
yield BtrfsFilesystemEntry(self.fs, entry_path, entry)
|
126
|
+
|
127
|
+
def is_dir(self, follow_symlinks: bool = True) -> bool:
|
128
|
+
try:
|
129
|
+
return self._resolve(follow_symlinks=follow_symlinks).entry.is_dir()
|
130
|
+
except FilesystemError:
|
131
|
+
return False
|
132
|
+
|
133
|
+
def is_file(self, follow_symlinks: bool = True) -> bool:
|
134
|
+
try:
|
135
|
+
return self._resolve(follow_symlinks=follow_symlinks).entry.is_file()
|
136
|
+
except FilesystemError:
|
137
|
+
return False
|
138
|
+
|
139
|
+
def is_symlink(self) -> bool:
|
140
|
+
return self.entry.is_symlink()
|
141
|
+
|
142
|
+
def readlink(self) -> str:
|
143
|
+
if not self.is_symlink():
|
144
|
+
raise NotASymlinkError()
|
145
|
+
|
146
|
+
return self.entry.link
|
147
|
+
|
148
|
+
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
149
|
+
return self._resolve(follow_symlinks=follow_symlinks).lstat()
|
150
|
+
|
151
|
+
def lstat(self) -> fsutil.stat_result:
|
152
|
+
entry = self.entry
|
153
|
+
node = self.entry.inode
|
154
|
+
|
155
|
+
# mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime
|
156
|
+
st_info = st_info = fsutil.stat_result(
|
157
|
+
[
|
158
|
+
entry.mode,
|
159
|
+
entry.inum,
|
160
|
+
0,
|
161
|
+
node.nlink,
|
162
|
+
entry.uid,
|
163
|
+
entry.gid,
|
164
|
+
entry.size,
|
165
|
+
# timestamp() returns a float which will fill both the integer and float fields
|
166
|
+
entry.atime.timestamp(),
|
167
|
+
entry.mtime.timestamp(),
|
168
|
+
entry.ctime.timestamp(),
|
169
|
+
]
|
170
|
+
)
|
171
|
+
|
172
|
+
# Set the nanosecond resolution separately
|
173
|
+
st_info.st_atime_ns = entry.atime_ns
|
174
|
+
st_info.st_mtime_ns = entry.mtime_ns
|
175
|
+
st_info.st_ctime_ns = entry.ctime_ns
|
176
|
+
|
177
|
+
# Btrfs has a birth time, called otime
|
178
|
+
st_info.st_birthtime = entry.otime.timestamp()
|
179
|
+
|
180
|
+
return st_info
|
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
|
|
@@ -199,10 +199,9 @@ class SymantecPlugin(Plugin):
|
|
199
199
|
|
200
200
|
def check_compatible(self) -> None:
|
201
201
|
for log_file in self.LOGS:
|
202
|
-
if self.target.fs.glob(log_file):
|
203
|
-
|
204
|
-
|
205
|
-
raise UnsupportedPluginError("No Symantec SEP logs found")
|
202
|
+
if list(self.target.fs.glob(log_file)):
|
203
|
+
return
|
204
|
+
raise UnsupportedPluginError("No Symantec SEP logs found")
|
206
205
|
|
207
206
|
def _fw_cell(self, line: list, cell_id: int) -> str:
|
208
207
|
return line[cell_id].decode("utf-8")
|
@@ -11,6 +11,7 @@ from flow.record.fieldtypes import posix_path
|
|
11
11
|
from dissect.target.filesystem import Filesystem
|
12
12
|
from dissect.target.helpers.fsutil import TargetPath
|
13
13
|
from dissect.target.helpers.record import UnixUserRecord
|
14
|
+
from dissect.target.helpers.utils import parse_options_string
|
14
15
|
from dissect.target.plugin import OperatingSystem, OSPlugin, arg, export
|
15
16
|
from dissect.target.target import Target
|
16
17
|
|
@@ -177,33 +178,41 @@ class UnixPlugin(OSPlugin):
|
|
177
178
|
def _add_mounts(self) -> None:
|
178
179
|
fstab = self.target.fs.path("/etc/fstab")
|
179
180
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
181
|
+
for dev_id, volume_name, mount_point, _, options in parse_fstab(fstab, self.target.log):
|
182
|
+
opts = parse_options_string(options)
|
183
|
+
subvol = opts.get("subvol", None)
|
184
|
+
subvolid = opts.get("subvolid", None)
|
185
|
+
for fs in self.target.filesystems:
|
184
186
|
fs_id = None
|
187
|
+
fs_subvol = None
|
188
|
+
fs_subvolid = None
|
189
|
+
fs_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
|
185
190
|
last_mount = None
|
186
191
|
|
187
192
|
if dev_id:
|
188
|
-
if
|
189
|
-
fs_id =
|
190
|
-
elif
|
191
|
-
fs_id =
|
192
|
-
last_mount =
|
193
|
-
elif
|
194
|
-
fs_id =
|
193
|
+
if fs.__type__ == "xfs":
|
194
|
+
fs_id = fs.xfs.uuid
|
195
|
+
elif fs.__type__ == "ext":
|
196
|
+
fs_id = fs.extfs.uuid
|
197
|
+
last_mount = fs.extfs.last_mount
|
198
|
+
elif fs.__type__ == "btrfs":
|
199
|
+
fs_id = fs.btrfs.uuid
|
200
|
+
fs_subvol = fs.subvolume.path
|
201
|
+
fs_subvolid = fs.subvolume.objectid
|
202
|
+
elif fs.__type__ == "fat":
|
203
|
+
fs_id = fs.fatfs.volume_id
|
195
204
|
# This normalizes fs_id to comply with libblkid generated UUIDs
|
196
205
|
# This is needed because FAT filesystems don't have a real UUID,
|
197
206
|
# but instead a volume_id which is not case-sensitive
|
198
207
|
fs_id = fs_id[:4].upper() + "-" + fs_id[4:].upper()
|
199
208
|
|
200
209
|
if (
|
201
|
-
(fs_id and (fs_id == dev_id))
|
202
|
-
or (
|
210
|
+
(fs_id and (fs_id == dev_id and (subvol == fs_subvol or subvolid == fs_subvolid)))
|
211
|
+
or (fs_volume_name and (fs_volume_name == volume_name))
|
203
212
|
or (last_mount and (last_mount == mount_point))
|
204
213
|
):
|
205
|
-
self.target.log.debug("Mounting %s at %s", volume, mount_point)
|
206
|
-
self.target.fs.mount(mount_point,
|
214
|
+
self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
|
215
|
+
self.target.fs.mount(mount_point, fs)
|
207
216
|
|
208
217
|
def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
|
209
218
|
"""Parse files containing Unix version information.
|
@@ -283,7 +292,7 @@ class UnixPlugin(OSPlugin):
|
|
283
292
|
def parse_fstab(
|
284
293
|
fstab: TargetPath,
|
285
294
|
log: logging.Logger = log,
|
286
|
-
) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str]]:
|
295
|
+
) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str, str]]:
|
287
296
|
"""Parse fstab file and return a generator that streams the details of entries,
|
288
297
|
with unsupported FS types and block devices filtered away.
|
289
298
|
"""
|
@@ -310,7 +319,7 @@ def parse_fstab(
|
|
310
319
|
if len(entry_parts) != 6:
|
311
320
|
continue
|
312
321
|
|
313
|
-
dev, mount_point, fs_type,
|
322
|
+
dev, mount_point, fs_type, options, _, _ = entry_parts
|
314
323
|
|
315
324
|
if fs_type in SKIP_FS_TYPES:
|
316
325
|
log.warning("Skipped FS type: %s, %s, %s", fs_type, dev, mount_point)
|
@@ -341,4 +350,4 @@ def parse_fstab(
|
|
341
350
|
except ValueError:
|
342
351
|
pass
|
343
352
|
|
344
|
-
yield dev_id, volume_name, fs_type,
|
353
|
+
yield dev_id, volume_name, mount_point, fs_type, options
|
dissect/target/target.py
CHANGED
@@ -155,6 +155,7 @@ class Target:
|
|
155
155
|
"""Resolve all disks, volumes and filesystems and load an operating system on the current ``Target``."""
|
156
156
|
self.disks.apply()
|
157
157
|
self.volumes.apply()
|
158
|
+
self.filesystems.apply()
|
158
159
|
self._init_os()
|
159
160
|
self._applied = True
|
160
161
|
|
@@ -712,19 +713,12 @@ class DiskCollection(Collection[container.Container]):
|
|
712
713
|
|
713
714
|
|
714
715
|
class VolumeCollection(Collection[volume.Volume]):
|
715
|
-
def open(self, vol: volume.Volume) -> None:
|
716
|
-
try:
|
717
|
-
if not hasattr(vol, "fs") or vol.fs is None:
|
718
|
-
vol.fs = filesystem.open(vol)
|
719
|
-
self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
|
720
|
-
self.target.filesystems.add(vol.fs)
|
721
|
-
except FilesystemError as e:
|
722
|
-
self.target.log.warning("Can't identify filesystem: %s", vol)
|
723
|
-
self.target.log.debug("", exc_info=e)
|
724
|
-
|
725
716
|
def apply(self) -> None:
|
726
717
|
# We don't want later additions to modify the todo, so make a copy
|
727
718
|
todo = self.entries[:]
|
719
|
+
fs_volumes = []
|
720
|
+
lvm_volumes = []
|
721
|
+
encrypted_volumes = []
|
728
722
|
|
729
723
|
while todo:
|
730
724
|
new_volumes = []
|
@@ -751,19 +745,19 @@ class VolumeCollection(Collection[volume.Volume]):
|
|
751
745
|
if vol.offset == 0 and vol.vs and vol.vs.__type__ == "disk":
|
752
746
|
# We are going to re-open a volume system on itself, bail out
|
753
747
|
self.target.log.info("Found volume with offset 0, opening as raw volume instead")
|
754
|
-
|
748
|
+
fs_volumes.append(vol)
|
755
749
|
continue
|
756
750
|
|
757
751
|
try:
|
758
752
|
vs = volume.open(vol)
|
759
753
|
except Exception:
|
760
754
|
# If opening a volume system fails, there's likely none, so open as a filesystem instead
|
761
|
-
|
755
|
+
fs_volumes.append(vol)
|
762
756
|
continue
|
763
757
|
|
764
758
|
if not len(vs.volumes):
|
765
759
|
# We opened an empty volume system, discard
|
766
|
-
|
760
|
+
fs_volumes.append(vol)
|
767
761
|
continue
|
768
762
|
|
769
763
|
self.entries.extend(vs.volumes)
|
@@ -786,20 +780,30 @@ class VolumeCollection(Collection[volume.Volume]):
|
|
786
780
|
|
787
781
|
todo = new_volumes
|
788
782
|
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
783
|
+
mv_fs_volumes = []
|
784
|
+
for vol in fs_volumes:
|
785
|
+
try:
|
786
|
+
if getattr(vol, "fs", None) is None:
|
787
|
+
if filesystem.is_multi_volume_filesystem(vol):
|
788
|
+
mv_fs_volumes.append(vol)
|
789
|
+
else:
|
790
|
+
vol.fs = filesystem.open(vol)
|
791
|
+
self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
|
795
792
|
|
796
|
-
|
797
|
-
|
793
|
+
if getattr(vol, "fs", None) is not None:
|
794
|
+
self.target.filesystems.add(vol.fs)
|
795
|
+
except FilesystemError as e:
|
796
|
+
self.target.log.warning("Can't identify filesystem: %s", vol)
|
797
|
+
self.target.log.debug("", exc_info=e)
|
798
798
|
|
799
|
-
|
800
|
-
|
801
|
-
|
799
|
+
for fs in filesystem.open_multi_volume(mv_fs_volumes):
|
800
|
+
self.target.filesystems.add(fs)
|
801
|
+
for vol in fs.volume:
|
802
|
+
vol.fs = fs
|
802
803
|
|
803
804
|
|
804
805
|
class FilesystemCollection(Collection[filesystem.Filesystem]):
|
805
|
-
|
806
|
+
def apply(self) -> None:
|
807
|
+
for fs in self.entries:
|
808
|
+
for subfs in fs.iter_subfs():
|
809
|
+
self.add(subfs)
|
dissect/target/tools/mount.py
CHANGED
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
from typing import Union
|
4
4
|
|
5
5
|
from dissect.target import Target, filesystem
|
6
|
+
from dissect.target.helpers.utils import parse_options_string
|
6
7
|
from dissect.target.tools.utils import (
|
7
8
|
catch_sigpipe,
|
8
9
|
configure_generic_arguments,
|
@@ -70,7 +71,7 @@ def main():
|
|
70
71
|
vfs.mount(fname, fs)
|
71
72
|
|
72
73
|
# This is kinda silly because fusepy will convert this back into string arguments
|
73
|
-
options =
|
74
|
+
options = parse_options_string(args.options) if args.options else {}
|
74
75
|
|
75
76
|
options["nothreads"] = True
|
76
77
|
options["allow_other"] = True
|
@@ -83,17 +84,6 @@ def main():
|
|
83
84
|
parser.exit("FUSE error")
|
84
85
|
|
85
86
|
|
86
|
-
def _parse_options(options: str) -> dict[str, Union[str, bool]]:
|
87
|
-
result = {}
|
88
|
-
for opt in options.split(","):
|
89
|
-
if "=" in opt:
|
90
|
-
key, _, value = opt.partition("=")
|
91
|
-
result[key] = value
|
92
|
-
else:
|
93
|
-
result[opt] = True
|
94
|
-
return result
|
95
|
-
|
96
|
-
|
97
87
|
def _format_options(options: dict[str, Union[str, bool]]) -> str:
|
98
88
|
return ",".join([key if value is True else f"{key}={value}" for key, value in options.items()])
|
99
89
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.14.
|
3
|
+
Version: 3.14.dev10
|
4
4
|
Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
6
6
|
License: Affero General Public License v3
|
@@ -38,6 +38,7 @@ Requires-Dist: dissect.target[full] ; extra == 'cb'
|
|
38
38
|
Requires-Dist: carbon-black-cloud-sdk-python ~=1.4.3 ; extra == 'cb'
|
39
39
|
Provides-Extra: full
|
40
40
|
Requires-Dist: asn1crypto ; extra == 'full'
|
41
|
+
Requires-Dist: dissect.btrfs <2.0.dev,>=1.0.dev ; extra == 'full'
|
41
42
|
Requires-Dist: dissect.cim <4.0.dev,>=3.0.dev ; extra == 'full'
|
42
43
|
Requires-Dist: dissect.clfs <2.0.dev,>=1.0.dev ; extra == 'full'
|
43
44
|
Requires-Dist: dissect.esedb <4.0.dev,>=3.0.dev ; extra == 'full'
|
@@ -1,11 +1,11 @@
|
|
1
1
|
dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
|
2
2
|
dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
|
3
3
|
dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
|
4
|
-
dissect/target/filesystem.py,sha256=
|
4
|
+
dissect/target/filesystem.py,sha256=_7hxYD34P5Dxnc7WZZsAHvQcF8KxGUJfGgc1gCyQZA8,53412
|
5
5
|
dissect/target/loader.py,sha256=4ZdX-QJY83NPswTyNG31LUwYXdV1tuByrR2vKKg7d5k,7214
|
6
6
|
dissect/target/plugin.py,sha256=7Gss9pofcWKemwwfeAJ7E6nmJSNnZkBkxTcxUY2wzmk,40526
|
7
7
|
dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
|
8
|
-
dissect/target/target.py,sha256=
|
8
|
+
dissect/target/target.py,sha256=zARjjT1tSMW4opble1KfDIISt3pcxPa50cTwhWC_90c,31431
|
9
9
|
dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,15801
|
10
10
|
dissect/target/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
dissect/target/containers/asdf.py,sha256=DJp0QEFwUjy2MFwKYcYqIR_BS1fQT1Yi9Kcmqt0aChM,1366
|
@@ -22,6 +22,7 @@ dissect/target/containers/vmdk.py,sha256=5fQGkJy4esXONXrKLbhpkQDt8Fwx19YENK2mOm7
|
|
22
22
|
dissect/target/data/autocompletion/target_bash_completion.sh,sha256=wrOQ_ED-h8WFcjCmY6n4qKl84tWJv9l8ShFHDfJqJyA,3592
|
23
23
|
dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
24
|
dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
|
25
|
+
dissect/target/filesystems/btrfs.py,sha256=X8HUZJHzArpo04v_A42LM4SQIfQt07TZJJNsYiSvXOE,6247
|
25
26
|
dissect/target/filesystems/cb.py,sha256=kFyZ7oFMkICSEGGFna2vhKnZx9KQKZ2ZSG_ckEWTIBE,5329
|
26
27
|
dissect/target/filesystems/config.py,sha256=Xih61MoEup1rCwQu66SezmTDG4XLOn1hC8ECn_WxX7I,5949
|
27
28
|
dissect/target/filesystems/dir.py,sha256=1RVT0-lomceRLQG5sXHpiyii4Vu7S1MGeQSyT-7BCPY,3851
|
@@ -56,7 +57,7 @@ dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhH
|
|
56
57
|
dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
|
57
58
|
dissect/target/helpers/ssh.py,sha256=LPssHXyfL8QYmLi2vpa3wElsGboLG_A1Y8kvOehpUr4,6338
|
58
59
|
dissect/target/helpers/targetd.py,sha256=ELhUulzQ4OgXgHsWhsLgM14vut8Wm6btr7qTynlwKaE,1812
|
59
|
-
dissect/target/helpers/utils.py,sha256=
|
60
|
+
dissect/target/helpers/utils.py,sha256=r36Bn0UL0E6Z8ajmQrHzC6RyUxTRdwJ1PNsd904Lmzs,4027
|
60
61
|
dissect/target/helpers/data/windowsZones.xml,sha256=4OijeR7oxI0ZwPTSwCkmtcofOsUCjSnbZ4dQxVOM_4o,50005
|
61
62
|
dissect/target/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
62
63
|
dissect/target/loaders/ad1.py,sha256=k0bkY0L6NKuQBboua-jM_KgeyIcXAo59NjExDZHMy0o,572
|
@@ -96,7 +97,7 @@ dissect/target/plugins/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
96
97
|
dissect/target/plugins/apps/av/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
97
98
|
dissect/target/plugins/apps/av/mcafee.py,sha256=4lro9iwcL2Vl9Lyy69Sk1D9JWSRTXv5yjpV6NJbbZXE,5409
|
98
99
|
dissect/target/plugins/apps/av/sophos.py,sha256=6a4N44VPwrXucGcTspwkCEhKPw5cfmqi-AQrXXDBKps,4140
|
99
|
-
dissect/target/plugins/apps/av/symantec.py,sha256=
|
100
|
+
dissect/target/plugins/apps/av/symantec.py,sha256=RFLyNW6FyuoGcirJ4xHbQM8oGjua9W4zXmC7YDF-H20,14109
|
100
101
|
dissect/target/plugins/apps/av/trendmicro.py,sha256=SDSFcjbabP3IORuVovoZNxTEpMw_rPxbm_JQ5Yg9xeQ,4649
|
101
102
|
dissect/target/plugins/apps/browser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
102
103
|
dissect/target/plugins/apps/browser/browser.py,sha256=LpGs4IXpO5t7A3QDxP3zv5Cb0DQXwh3h1vcBKAWT2HY,2187
|
@@ -159,7 +160,7 @@ dissect/target/plugins/general/scrape.py,sha256=Fz7BNXflvuxlnVulyyDhLpyU8D_hJdH6
|
|
159
160
|
dissect/target/plugins/general/users.py,sha256=IOqopQ9Y7CKGkALRUr16y8DwxsidYC5tcPErGZCXxyA,2845
|
160
161
|
dissect/target/plugins/os/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
161
162
|
dissect/target/plugins/os/unix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
162
|
-
dissect/target/plugins/os/unix/_os.py,sha256=
|
163
|
+
dissect/target/plugins/os/unix/_os.py,sha256=33xAyHYG5vqpMupoIk_vbwsxAXBBRV1hqwkOOU5Gvg8,13409
|
163
164
|
dissect/target/plugins/os/unix/cronjobs.py,sha256=tVRnUxN0w2IqhOKs68hNeovRUu_ag0W35j9PziPprgQ,3508
|
164
165
|
dissect/target/plugins/os/unix/datetime.py,sha256=gKfBdPyUirt3qmVYfOJ1oZXRPn8wRzssbZxR_ARrtk8,1518
|
165
166
|
dissect/target/plugins/os/unix/etc.py,sha256=HoPEC1hxqurSnAXQAK-jf_HxdBIDe-1z_qSw_n-ViI4,258
|
@@ -286,7 +287,7 @@ dissect/target/tools/dd.py,sha256=Nlh2CFOCV0ksxyedFp7BuyoQ3tBFi6rK6UO0_k5GR_8,17
|
|
286
287
|
dissect/target/tools/fs.py,sha256=IL71ntXA_oS92l0NPpqyOVrHOZ-bf3qag1amZzAaeHc,3548
|
287
288
|
dissect/target/tools/info.py,sha256=LcR0WLBo2w0QiOmM0llQfcF0a_hfrqImbJ_QNW1mtbo,5174
|
288
289
|
dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLcEg,4174
|
289
|
-
dissect/target/tools/mount.py,sha256=
|
290
|
+
dissect/target/tools/mount.py,sha256=m6Ise8H82jgIW2FN0hXKO4l9t3emKiOi55O4LyEqvxk,2581
|
290
291
|
dissect/target/tools/query.py,sha256=qbQI2kAeFP0_1CxT3UbTIZZ1EZIhotD0rRNXqihZcy4,14926
|
291
292
|
dissect/target/tools/reg.py,sha256=ZB5WDmKfiDvs988kHZVyzF-RIoDLXcXuvdLjuEYvDi4,2181
|
292
293
|
dissect/target/tools/shell.py,sha256=zgabhrXMBFA36g7Rc-8VkF5BL6xc4148vblYnjfI88I,42281
|
@@ -303,10 +304,10 @@ dissect/target/volumes/luks.py,sha256=v_mHW05KM5iG8JDe47i2V4Q9O0r4rnAMA9m_qc9cYw
|
|
303
304
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
304
305
|
dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
|
305
306
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
306
|
-
dissect.target-3.14.
|
307
|
-
dissect.target-3.14.
|
308
|
-
dissect.target-3.14.
|
309
|
-
dissect.target-3.14.
|
310
|
-
dissect.target-3.14.
|
311
|
-
dissect.target-3.14.
|
312
|
-
dissect.target-3.14.
|
307
|
+
dissect.target-3.14.dev10.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
308
|
+
dissect.target-3.14.dev10.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
309
|
+
dissect.target-3.14.dev10.dist-info/METADATA,sha256=k9I3m8_nSD3iQchZl7cyxC4H3_Hu6EFQAoszbRjdevg,11042
|
310
|
+
dissect.target-3.14.dev10.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
311
|
+
dissect.target-3.14.dev10.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
|
312
|
+
dissect.target-3.14.dev10.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
313
|
+
dissect.target-3.14.dev10.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|