dissect.target 3.16.dev44__py3-none-any.whl → 3.17__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/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/general/users.py +6 -0
- 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.dev44.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/RECORD +63 -56
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
dissect/target/filesystem.py
CHANGED
@@ -525,21 +525,21 @@ class FilesystemEntry:
|
|
525
525
|
follow_symlinks: Whether to resolve the entry if it is a symbolic link.
|
526
526
|
|
527
527
|
Returns:
|
528
|
-
The resolved symbolic link if ``follow_symlinks`` is ``True`` and the
|
529
|
-
symbolic link or else the
|
528
|
+
The resolved symbolic link if ``follow_symlinks`` is ``True`` and the :class:`FilesystemEntry` is a
|
529
|
+
symbolic link or else the :class:`FilesystemEntry` itself.
|
530
530
|
"""
|
531
531
|
if follow_symlinks and self.is_symlink():
|
532
532
|
return self.readlink_ext()
|
533
533
|
return self
|
534
534
|
|
535
535
|
def get(self, path: str) -> FilesystemEntry:
|
536
|
-
"""Retrieve a FilesystemEntry relative to this entry.
|
536
|
+
"""Retrieve a :class:`FilesystemEntry` relative to this entry.
|
537
537
|
|
538
538
|
Args:
|
539
539
|
path: The path relative to this filesystem entry.
|
540
540
|
|
541
541
|
Returns:
|
542
|
-
A relative FilesystemEntry
|
542
|
+
A relative :class:`FilesystemEntry`.
|
543
543
|
"""
|
544
544
|
raise NotImplementedError()
|
545
545
|
|
@@ -560,10 +560,10 @@ class FilesystemEntry:
|
|
560
560
|
raise NotImplementedError()
|
561
561
|
|
562
562
|
def scandir(self) -> Iterator[FilesystemEntry]:
|
563
|
-
"""Iterate over the contents of a directory,
|
563
|
+
"""Iterate over the contents of a directory, yields :class:`FilesystemEntry`.
|
564
564
|
|
565
565
|
Returns:
|
566
|
-
An iterator of
|
566
|
+
An iterator of :class:`FilesystemEntry`.
|
567
567
|
"""
|
568
568
|
raise NotImplementedError()
|
569
569
|
|
@@ -576,10 +576,10 @@ class FilesystemEntry:
|
|
576
576
|
return list(self.iterdir())
|
577
577
|
|
578
578
|
def listdir_ext(self) -> list[FilesystemEntry]:
|
579
|
-
"""List the contents of a directory as FilesystemEntry
|
579
|
+
"""List the contents of a directory as a list of :class:`FilesystemEntry`.
|
580
580
|
|
581
581
|
Returns:
|
582
|
-
A list of FilesystemEntry
|
582
|
+
A list of :class:`FilesystemEntry`.
|
583
583
|
"""
|
584
584
|
return list(self.scandir())
|
585
585
|
|
@@ -614,7 +614,7 @@ class FilesystemEntry:
|
|
614
614
|
onerror: Optional[Callable] = None,
|
615
615
|
followlinks: bool = False,
|
616
616
|
) -> Iterator[FilesystemEntry]:
|
617
|
-
"""Walk a directory and show its contents as FilesystemEntry
|
617
|
+
"""Walk a directory and show its contents as :class:`FilesystemEntry`.
|
618
618
|
|
619
619
|
It walks across all the files inside the entry recursively.
|
620
620
|
|
@@ -629,11 +629,11 @@ class FilesystemEntry:
|
|
629
629
|
followlinks: ``True`` if we want to follow any symbolic link
|
630
630
|
|
631
631
|
Returns:
|
632
|
-
An iterator of
|
632
|
+
An iterator of :class:`FilesystemEntry`.
|
633
633
|
"""
|
634
634
|
yield from fsutil.walk_ext(self, topdown, onerror, followlinks)
|
635
635
|
|
636
|
-
def glob(self, pattern) -> Iterator[str]:
|
636
|
+
def glob(self, pattern: str) -> Iterator[str]:
|
637
637
|
"""Iterate over this directory part of ``patern``, returning entries matching ``pattern`` as strings.
|
638
638
|
|
639
639
|
Args:
|
@@ -645,21 +645,23 @@ class FilesystemEntry:
|
|
645
645
|
for entry in self.glob_ext(pattern):
|
646
646
|
yield entry.path
|
647
647
|
|
648
|
-
def glob_ext(self, pattern) -> Iterator[FilesystemEntry]:
|
649
|
-
"""Iterate over the directory part of ``pattern``, returning entries matching
|
648
|
+
def glob_ext(self, pattern: str) -> Iterator[FilesystemEntry]:
|
649
|
+
"""Iterate over the directory part of ``pattern``, returning entries matching
|
650
|
+
``pattern`` as :class:`FilesysmteEntry`.
|
650
651
|
|
651
652
|
Args:
|
652
653
|
pattern: The pattern to glob for.
|
653
654
|
|
654
655
|
Returns:
|
655
|
-
An iterator of FilesystemEntry
|
656
|
+
An iterator of :class:`FilesystemEntry` that match the pattern.
|
656
657
|
"""
|
657
658
|
yield from fsutil.glob_ext(self, pattern)
|
658
659
|
|
659
660
|
def exists(self, path: str) -> bool:
|
660
661
|
"""Determines whether a ``path``, relative to this entry, exists.
|
661
662
|
|
662
|
-
If the `path` is a symbolic link, it will attempt to resolve it to find
|
663
|
+
If the `path` is a symbolic link, it will attempt to resolve it to find
|
664
|
+
the :class:`FilesystemEntry` it points to.
|
663
665
|
|
664
666
|
Args:
|
665
667
|
path: The path relative to this entry.
|
@@ -737,7 +739,7 @@ class FilesystemEntry:
|
|
737
739
|
raise NotImplementedError()
|
738
740
|
|
739
741
|
def readlink_ext(self) -> FilesystemEntry:
|
740
|
-
"""Read the link where this entry points to, return the resulting path as FilesystemEntry
|
742
|
+
"""Read the link where this entry points to, return the resulting path as :class:`FilesystemEntry`.
|
741
743
|
|
742
744
|
If it is a symlink and returns the string that corresponds to that path.
|
743
745
|
This means it follows the path a link points to, it tries to do it recursively.
|
@@ -860,7 +862,7 @@ class VirtualDirectory(FilesystemEntry):
|
|
860
862
|
raise TypeError(f"lattr is not allowed on VirtualDirectory: {self.path}")
|
861
863
|
|
862
864
|
def add(self, name: str, entry: FilesystemEntry) -> None:
|
863
|
-
"""Add an entry to this VirtualDirectory
|
865
|
+
"""Add an entry to this :class:`VirtualDirectory`."""
|
864
866
|
if not self.fs.case_sensitive:
|
865
867
|
name = name.lower()
|
866
868
|
|
@@ -1214,7 +1216,7 @@ class VirtualFilesystem(Filesystem):
|
|
1214
1216
|
self.map_file_entry(vfspath, VirtualFile(self, file_path, fh))
|
1215
1217
|
|
1216
1218
|
def map_file_entry(self, vfspath: str, entry: FilesystemEntry) -> None:
|
1217
|
-
"""Map a FilesystemEntry into the VFS.
|
1219
|
+
"""Map a :class:`FilesystemEntry` into the VFS.
|
1218
1220
|
|
1219
1221
|
Any missing subdirectories up to, but not including, the last part of
|
1220
1222
|
``vfspath`` will be created.
|
@@ -1271,7 +1273,7 @@ class VirtualFilesystem(Filesystem):
|
|
1271
1273
|
return self.map_dir_from_tar(vfspath.lstrip("/"), tar_file, map_single_file=True)
|
1272
1274
|
|
1273
1275
|
def link(self, src: str, dst: str) -> None:
|
1274
|
-
"""Hard link a FilesystemEntry to another location.
|
1276
|
+
"""Hard link a :class:`FilesystemEntry` to another location.
|
1275
1277
|
|
1276
1278
|
Args:
|
1277
1279
|
src: The path to the target of the link.
|
@@ -1291,65 +1293,132 @@ class VirtualFilesystem(Filesystem):
|
|
1291
1293
|
self.map_file_entry(dst, VirtualSymlink(self, dst, src))
|
1292
1294
|
|
1293
1295
|
|
1294
|
-
class
|
1295
|
-
__type__ = "
|
1296
|
+
class LayerFilesystem(Filesystem):
|
1297
|
+
__type__ = "layer"
|
1296
1298
|
|
1297
|
-
def __init__(self,
|
1298
|
-
self.
|
1299
|
-
self.layers = []
|
1299
|
+
def __init__(self, **kwargs):
|
1300
|
+
self.layers: list[Filesystem] = []
|
1300
1301
|
self.mounts = {}
|
1301
1302
|
self._alt_separator = "/"
|
1302
1303
|
self._case_sensitive = True
|
1303
|
-
self._root_entry =
|
1304
|
-
self.root = self.
|
1305
|
-
super().__init__(None)
|
1304
|
+
self._root_entry = LayerFilesystemEntry(self, "/", [])
|
1305
|
+
self.root = self.append_layer()
|
1306
|
+
super().__init__(None, **kwargs)
|
1307
|
+
|
1308
|
+
def __getattr__(self, attr: str) -> Any:
|
1309
|
+
"""Provide "magic" access to filesystem specific attributes from any of the layers.
|
1310
|
+
|
1311
|
+
For example, on a :class:`LayerFilesystem` ``fs``, you can do ``fs.ntfs`` to access the
|
1312
|
+
internal NTFS object if it has an NTFS layer.
|
1313
|
+
"""
|
1314
|
+
for fs in self.layers:
|
1315
|
+
try:
|
1316
|
+
return getattr(fs, attr)
|
1317
|
+
except AttributeError:
|
1318
|
+
continue
|
1319
|
+
else:
|
1320
|
+
return object.__getattribute__(self, attr)
|
1306
1321
|
|
1307
1322
|
@staticmethod
|
1308
1323
|
def detect(fh: BinaryIO) -> bool:
|
1309
|
-
raise TypeError("Detect is not allowed on
|
1324
|
+
raise TypeError("Detect is not allowed on LayerFilesystem class")
|
1310
1325
|
|
1311
|
-
def mount(self, path: str, fs: Filesystem) -> None:
|
1326
|
+
def mount(self, path: str, fs: Filesystem, ignore_existing: bool = True) -> None:
|
1312
1327
|
"""Mount a filesystem at a given path.
|
1313
1328
|
|
1314
1329
|
If there's an overlap with an existing mount, creates a new layer.
|
1330
|
+
|
1331
|
+
Args:
|
1332
|
+
path: The path to mount the filesystem at.
|
1333
|
+
fs: The filesystem to mount.
|
1334
|
+
ignore_existing: Whether to ignore existing mounts and create a new layer. Defaults to ``True``.
|
1315
1335
|
"""
|
1316
1336
|
root = self.root
|
1317
1337
|
for mount in self.mounts.keys():
|
1318
|
-
if path == mount:
|
1338
|
+
if ignore_existing and path == mount:
|
1319
1339
|
continue
|
1320
1340
|
|
1321
1341
|
if path.startswith(mount):
|
1322
|
-
root = self.
|
1342
|
+
root = self.append_layer()
|
1323
1343
|
break
|
1324
1344
|
|
1325
1345
|
root.map_fs(path, fs)
|
1326
1346
|
self.mounts[path] = fs
|
1327
1347
|
|
1328
1348
|
def link(self, dst: str, src: str) -> None:
|
1329
|
-
"""Hard link a
|
1330
|
-
dst = fsutil.normalize(dst, alt_separator=self.alt_separator)
|
1349
|
+
"""Hard link a :class:`FilesystemEntry` to another location."""
|
1331
1350
|
self.root.map_file_entry(dst, self.get(src))
|
1332
1351
|
|
1333
1352
|
def symlink(self, dst: str, src: str) -> None:
|
1334
1353
|
"""Create a symlink to another location."""
|
1335
1354
|
self.root.symlink(dst, src)
|
1336
1355
|
|
1337
|
-
def
|
1356
|
+
def append_layer(self, **kwargs) -> VirtualFilesystem:
|
1357
|
+
"""Append a new layer."""
|
1358
|
+
layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs)
|
1359
|
+
self.append_fs_layer(layer)
|
1360
|
+
return layer
|
1361
|
+
|
1362
|
+
add_layer = append_layer
|
1363
|
+
|
1364
|
+
def prepend_layer(self, **kwargs) -> VirtualFilesystem:
|
1365
|
+
"""Prepend a new layer."""
|
1338
1366
|
layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs)
|
1339
|
-
self.
|
1340
|
-
self._root_entry.entries.append(layer.root)
|
1367
|
+
self.prepend_fs_layer(layer)
|
1341
1368
|
return layer
|
1342
1369
|
|
1370
|
+
def append_fs_layer(self, fs: Filesystem) -> None:
|
1371
|
+
"""Append a filesystem as a layer.
|
1372
|
+
|
1373
|
+
Args:
|
1374
|
+
fs: The filesystem to append.
|
1375
|
+
"""
|
1376
|
+
# Counterintuitively, we prepend the filesystem to the list of layers
|
1377
|
+
# We could reverse the list of layers upon iteration, but that is a hot path
|
1378
|
+
self.layers.insert(0, fs)
|
1379
|
+
self._root_entry.entries.insert(0, fs.get("/"))
|
1380
|
+
|
1381
|
+
def prepend_fs_layer(self, fs: Filesystem) -> None:
|
1382
|
+
"""Prepend a filesystem as a layer.
|
1383
|
+
|
1384
|
+
Args:
|
1385
|
+
fs: The filesystem to prepend.
|
1386
|
+
"""
|
1387
|
+
# Counterintuitively, we append the filesystem to the list of layers
|
1388
|
+
# We could reverse the list of layers upon iteration, but that is a hot path
|
1389
|
+
self.layers.append(fs)
|
1390
|
+
self._root_entry.entries.append(fs.get("/"))
|
1391
|
+
|
1392
|
+
def remove_fs_layer(self, fs: Filesystem) -> None:
|
1393
|
+
"""Remove a filesystem layer.
|
1394
|
+
|
1395
|
+
Args:
|
1396
|
+
fs: The filesystem to remove.
|
1397
|
+
"""
|
1398
|
+
self.remove_layer(self.layers.index(fs))
|
1399
|
+
|
1400
|
+
def remove_layer(self, idx: int) -> None:
|
1401
|
+
"""Remove a layer by index.
|
1402
|
+
|
1403
|
+
Args:
|
1404
|
+
idx: The index of the layer to remove.
|
1405
|
+
"""
|
1406
|
+
del self.layers[idx]
|
1407
|
+
del self._root_entry.entries[idx]
|
1408
|
+
|
1343
1409
|
@property
|
1344
1410
|
def case_sensitive(self) -> bool:
|
1411
|
+
"""Whether the filesystem is case sensitive."""
|
1345
1412
|
return self._case_sensitive
|
1346
1413
|
|
1347
1414
|
@property
|
1348
1415
|
def alt_separator(self) -> str:
|
1416
|
+
"""The alternative separator of the filesystem."""
|
1349
1417
|
return self._alt_separator
|
1350
1418
|
|
1351
1419
|
@case_sensitive.setter
|
1352
1420
|
def case_sensitive(self, value: bool) -> None:
|
1421
|
+
"""Set the case sensitivity of the filesystem (and all layers)."""
|
1353
1422
|
self._case_sensitive = value
|
1354
1423
|
self.root.case_sensitive = value
|
1355
1424
|
for layer in self.layers:
|
@@ -1357,14 +1426,14 @@ class RootFilesystem(Filesystem):
|
|
1357
1426
|
|
1358
1427
|
@alt_separator.setter
|
1359
1428
|
def alt_separator(self, value: str) -> None:
|
1429
|
+
"""Set the alternative separator of the filesystem (and all layers)."""
|
1360
1430
|
self._alt_separator = value
|
1361
1431
|
self.root.alt_separator = value
|
1362
1432
|
for layer in self.layers:
|
1363
1433
|
layer.alt_separator = value
|
1364
1434
|
|
1365
|
-
def get(self, path: str, relentry:
|
1366
|
-
|
1367
|
-
|
1435
|
+
def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> LayerFilesystemEntry:
|
1436
|
+
"""Get a :class:`FilesystemEntry` from the filesystem."""
|
1368
1437
|
entry = relentry or self._root_entry
|
1369
1438
|
path = fsutil.normalize(path, alt_separator=self.alt_separator).strip("/")
|
1370
1439
|
full_path = fsutil.join(entry.path, path, alt_separator=self.alt_separator)
|
@@ -1388,9 +1457,10 @@ class RootFilesystem(Filesystem):
|
|
1388
1457
|
raise NotASymlinkError(full_path)
|
1389
1458
|
raise FileNotFoundError(full_path)
|
1390
1459
|
|
1391
|
-
return
|
1460
|
+
return LayerFilesystemEntry(self, full_path, entries)
|
1392
1461
|
|
1393
1462
|
def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry:
|
1463
|
+
"""Get a :class:`FilesystemEntry` relative to a specific entry."""
|
1394
1464
|
parts = path.split("/")
|
1395
1465
|
|
1396
1466
|
for part in parts:
|
@@ -1405,11 +1475,11 @@ class RootFilesystem(Filesystem):
|
|
1405
1475
|
class EntryList(list):
|
1406
1476
|
"""Wrapper list for filesystem entries.
|
1407
1477
|
|
1408
|
-
|
1409
|
-
|
1478
|
+
Exposes a ``__getattr__`` on a list of items. Useful to access internal objects from filesystem implementations.
|
1479
|
+
For example, access the underlying NTFS object from a list of virtual and NTFS entries.
|
1410
1480
|
"""
|
1411
1481
|
|
1412
|
-
def __init__(self, value:
|
1482
|
+
def __init__(self, value: FilesystemEntry | list[FilesystemEntry]):
|
1413
1483
|
if not isinstance(value, list):
|
1414
1484
|
value = [value]
|
1415
1485
|
super().__init__(value)
|
@@ -1424,20 +1494,12 @@ class EntryList(list):
|
|
1424
1494
|
return object.__getattribute__(self, attr)
|
1425
1495
|
|
1426
1496
|
|
1427
|
-
class
|
1497
|
+
class LayerFilesystemEntry(FilesystemEntry):
|
1428
1498
|
def __init__(self, fs: Filesystem, path: str, entry: FilesystemEntry):
|
1429
1499
|
super().__init__(fs, path, EntryList(entry))
|
1430
|
-
self.entries = self.entry
|
1500
|
+
self.entries: EntryList = self.entry
|
1431
1501
|
self._link = None
|
1432
1502
|
|
1433
|
-
def __getattr__(self, attr):
|
1434
|
-
for entry in self.entries:
|
1435
|
-
try:
|
1436
|
-
return getattr(entry, attr)
|
1437
|
-
except AttributeError:
|
1438
|
-
continue
|
1439
|
-
return object.__getattribute__(self, attr)
|
1440
|
-
|
1441
1503
|
def _exec(self, func: str, *args, **kwargs) -> Any:
|
1442
1504
|
"""Helper method to execute a method over all contained entries."""
|
1443
1505
|
exc = []
|
@@ -1451,18 +1513,16 @@ class RootFilesystemEntry(FilesystemEntry):
|
|
1451
1513
|
exceptions = ",".join(exc)
|
1452
1514
|
else:
|
1453
1515
|
exceptions = "No entries"
|
1516
|
+
|
1454
1517
|
raise FilesystemError(f"Can't resolve {func} for {self}: {exceptions}")
|
1455
1518
|
|
1456
1519
|
def get(self, path: str) -> FilesystemEntry:
|
1457
|
-
self.fs.target.log.debug("%r::get(%r)", self, path)
|
1458
1520
|
return self.fs.get(path, self._resolve())
|
1459
1521
|
|
1460
1522
|
def open(self) -> BinaryIO:
|
1461
|
-
self.fs.target.log.debug("%r::open()", self)
|
1462
1523
|
return self._resolve()._exec("open")
|
1463
1524
|
|
1464
1525
|
def iterdir(self) -> Iterator[str]:
|
1465
|
-
self.fs.target.log.debug("%r::iterdir()", self)
|
1466
1526
|
yielded = {".", ".."}
|
1467
1527
|
selfentry = self._resolve()
|
1468
1528
|
for fsentry in selfentry.entries:
|
@@ -1474,8 +1534,7 @@ class RootFilesystemEntry(FilesystemEntry):
|
|
1474
1534
|
yield entry_name
|
1475
1535
|
yielded.add(name)
|
1476
1536
|
|
1477
|
-
def scandir(self) -> Iterator[
|
1478
|
-
self.fs.target.log.debug("%r::scandir()", self)
|
1537
|
+
def scandir(self) -> Iterator[LayerFilesystemEntry]:
|
1479
1538
|
# Every entry is actually a list of entries from the different
|
1480
1539
|
# overlaying FSes, of which each may implement a different function
|
1481
1540
|
# like .stat() or .open()
|
@@ -1495,49 +1554,115 @@ class RootFilesystemEntry(FilesystemEntry):
|
|
1495
1554
|
# overlaying FSes may have different casing of the name.
|
1496
1555
|
entry_name = entries[0].name
|
1497
1556
|
path = fsutil.join(selfentry.path, entry_name, alt_separator=selfentry.fs.alt_separator)
|
1498
|
-
yield
|
1557
|
+
yield LayerFilesystemEntry(selfentry.fs, path, entries)
|
1499
1558
|
|
1500
1559
|
def is_file(self, follow_symlinks: bool = True) -> bool:
|
1501
|
-
self.fs.target.log.debug("%r::is_file()", self)
|
1502
1560
|
try:
|
1503
1561
|
return self._resolve(follow_symlinks=follow_symlinks)._exec("is_file", follow_symlinks=follow_symlinks)
|
1504
1562
|
except FileNotFoundError:
|
1505
1563
|
return False
|
1506
1564
|
|
1507
1565
|
def is_dir(self, follow_symlinks: bool = True) -> bool:
|
1508
|
-
self.fs.target.log.debug("%r::is_dir()", self)
|
1509
1566
|
try:
|
1510
1567
|
return self._resolve(follow_symlinks=follow_symlinks)._exec("is_dir", follow_symlinks=follow_symlinks)
|
1511
1568
|
except FileNotFoundError:
|
1512
1569
|
return False
|
1513
1570
|
|
1514
1571
|
def is_symlink(self) -> bool:
|
1515
|
-
self.fs.target.log.debug("%r::is_symlink()", self)
|
1516
1572
|
return self._exec("is_symlink")
|
1517
1573
|
|
1518
1574
|
def readlink(self) -> str:
|
1519
|
-
self.fs.target.log.debug("%r::readlink()", self)
|
1520
1575
|
if not self.is_symlink():
|
1521
1576
|
raise NotASymlinkError(f"Not a link: {self}")
|
1522
1577
|
return self._exec("readlink")
|
1523
1578
|
|
1524
1579
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
1525
|
-
self.fs.target.log.debug("%r::stat()", self)
|
1526
1580
|
return self._resolve(follow_symlinks=follow_symlinks)._exec("stat", follow_symlinks=follow_symlinks)
|
1527
1581
|
|
1528
1582
|
def lstat(self) -> fsutil.stat_result:
|
1529
|
-
self.fs.target.log.debug("%r::lstat()", self)
|
1530
1583
|
return self._exec("lstat")
|
1531
1584
|
|
1532
1585
|
def attr(self) -> Any:
|
1533
|
-
self.fs.target.log.debug("%r::attr()", self)
|
1534
1586
|
return self._resolve()._exec("attr")
|
1535
1587
|
|
1536
1588
|
def lattr(self) -> Any:
|
1537
|
-
self.fs.target.log.debug("%r::lattr()", self)
|
1538
1589
|
return self._exec("lattr")
|
1539
1590
|
|
1540
1591
|
|
1592
|
+
class RootFilesystem(LayerFilesystem):
|
1593
|
+
__type__ = "root"
|
1594
|
+
|
1595
|
+
def __init__(self, target: Target):
|
1596
|
+
self.target = target
|
1597
|
+
super().__init__()
|
1598
|
+
|
1599
|
+
@staticmethod
|
1600
|
+
def detect(fh: BinaryIO) -> bool:
|
1601
|
+
raise TypeError("Detect is not allowed on RootFilesystem class")
|
1602
|
+
|
1603
|
+
def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> RootFilesystemEntry:
|
1604
|
+
self.target.log.debug("%r::get(%r)", self, path)
|
1605
|
+
entry = super().get(path, relentry)
|
1606
|
+
entry.__class__ = RootFilesystemEntry
|
1607
|
+
return entry
|
1608
|
+
|
1609
|
+
|
1610
|
+
class RootFilesystemEntry(LayerFilesystemEntry):
|
1611
|
+
fs: RootFilesystem
|
1612
|
+
|
1613
|
+
def get(self, path: str) -> RootFilesystemEntry:
|
1614
|
+
self.fs.target.log.debug("%r::get(%r)", self, path)
|
1615
|
+
entry = super().get(path)
|
1616
|
+
entry.__class__ = RootFilesystemEntry
|
1617
|
+
return entry
|
1618
|
+
|
1619
|
+
def open(self) -> BinaryIO:
|
1620
|
+
self.fs.target.log.debug("%r::open()", self)
|
1621
|
+
return super().open()
|
1622
|
+
|
1623
|
+
def iterdir(self) -> Iterator[str]:
|
1624
|
+
self.fs.target.log.debug("%r::iterdir()", self)
|
1625
|
+
yield from super().iterdir()
|
1626
|
+
|
1627
|
+
def scandir(self) -> Iterator[RootFilesystemEntry]:
|
1628
|
+
self.fs.target.log.debug("%r::scandir()", self)
|
1629
|
+
for entry in super().scandir():
|
1630
|
+
entry.__class__ = RootFilesystemEntry
|
1631
|
+
yield entry
|
1632
|
+
|
1633
|
+
def is_file(self, follow_symlinks: bool = True) -> bool:
|
1634
|
+
self.fs.target.log.debug("%r::is_file()", self)
|
1635
|
+
return super().is_file(follow_symlinks=follow_symlinks)
|
1636
|
+
|
1637
|
+
def is_dir(self, follow_symlinks: bool = True) -> bool:
|
1638
|
+
self.fs.target.log.debug("%r::is_dir()", self)
|
1639
|
+
return super().is_dir(follow_symlinks=follow_symlinks)
|
1640
|
+
|
1641
|
+
def is_symlink(self) -> bool:
|
1642
|
+
self.fs.target.log.debug("%r::is_symlink()", self)
|
1643
|
+
return super().is_symlink()
|
1644
|
+
|
1645
|
+
def readlink(self) -> str:
|
1646
|
+
self.fs.target.log.debug("%r::readlink()", self)
|
1647
|
+
return super().readlink()
|
1648
|
+
|
1649
|
+
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
1650
|
+
self.fs.target.log.debug("%r::stat()", self)
|
1651
|
+
return super().stat(follow_symlinks=follow_symlinks)
|
1652
|
+
|
1653
|
+
def lstat(self) -> fsutil.stat_result:
|
1654
|
+
self.fs.target.log.debug("%r::lstat()", self)
|
1655
|
+
return super().lstat()
|
1656
|
+
|
1657
|
+
def attr(self) -> Any:
|
1658
|
+
self.fs.target.log.debug("%r::attr()", self)
|
1659
|
+
return super().attr()
|
1660
|
+
|
1661
|
+
def lattr(self) -> Any:
|
1662
|
+
self.fs.target.log.debug("%r::lattr()", self)
|
1663
|
+
return super().lattr()
|
1664
|
+
|
1665
|
+
|
1541
1666
|
def register(module: str, class_name: str, internal: bool = True) -> None:
|
1542
1667
|
"""Register a filesystem implementation to use when opening a filesystem.
|
1543
1668
|
|
@@ -7,6 +7,7 @@ from dissect.target.exceptions import (
|
|
7
7
|
FilesystemError,
|
8
8
|
IsADirectoryError,
|
9
9
|
NotADirectoryError,
|
10
|
+
NotASymlinkError,
|
10
11
|
)
|
11
12
|
from dissect.target.filesystem import Filesystem, FilesystemEntry
|
12
13
|
from dissect.target.helpers import fsutil
|
@@ -55,6 +56,8 @@ class DirectoryFilesystem(Filesystem):
|
|
55
56
|
|
56
57
|
|
57
58
|
class DirectoryFilesystemEntry(FilesystemEntry):
|
59
|
+
entry: Path
|
60
|
+
|
58
61
|
def get(self, path: str) -> FilesystemEntry:
|
59
62
|
path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
|
60
63
|
return self.fs.get(path)
|
@@ -113,7 +116,17 @@ class DirectoryFilesystemEntry(FilesystemEntry):
|
|
113
116
|
return False
|
114
117
|
|
115
118
|
def readlink(self) -> str:
|
116
|
-
|
119
|
+
if not self.is_symlink():
|
120
|
+
raise NotASymlinkError()
|
121
|
+
|
122
|
+
# We want to get the "truest" form of the symlink
|
123
|
+
# If we use the readlink() of pathlib.Path directly, it gets thrown into the path parsing of pathlib
|
124
|
+
# Because DirectoryFilesystem may also be used with TargetPath, we specifically handle that case here
|
125
|
+
# and use os.readlink for host paths
|
126
|
+
if isinstance(self.entry, fsutil.TargetPath):
|
127
|
+
return self.entry.get().readlink()
|
128
|
+
else:
|
129
|
+
return os.readlink(self.entry)
|
117
130
|
|
118
131
|
def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
|
119
132
|
return self._resolve(follow_symlinks=follow_symlinks).entry.lstat()
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from dissect.target.filesystem import LayerFilesystem, VirtualFilesystem
|
6
|
+
from dissect.target.filesystems.dir import DirectoryFilesystem
|
7
|
+
|
8
|
+
log = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
class Overlay2Filesystem(LayerFilesystem):
|
12
|
+
"""Overlay 2 filesystem implementation.
|
13
|
+
|
14
|
+
Deleted files will be present on the reconstructed filesystem.
|
15
|
+
Volumes and bind mounts will be added to their respective mount locations.
|
16
|
+
Does not support tmpfs mounts.
|
17
|
+
|
18
|
+
References:
|
19
|
+
- https://docs.docker.com/storage/storagedriver/
|
20
|
+
- https://docs.docker.com/storage/volumes/
|
21
|
+
- https://www.didactic-security.com/resources/docker-forensics.pdf
|
22
|
+
- https://www.didactic-security.com/resources/docker-forensics-cheatsheet.pdf
|
23
|
+
- https://github.com/google/docker-explorer
|
24
|
+
"""
|
25
|
+
|
26
|
+
__type__ = "overlay2"
|
27
|
+
|
28
|
+
def __init__(self, path: Path, *args, **kwargs):
|
29
|
+
super().__init__(*args, **kwargs)
|
30
|
+
self.base_path = path
|
31
|
+
|
32
|
+
# base_path is /foo/bar/image/overlay2/layerdb/mounts/<id> so we traverse up to /foo/bar to get to the root.
|
33
|
+
root = path.parents[4]
|
34
|
+
|
35
|
+
layers = []
|
36
|
+
parent_layer = path.joinpath("parent").read_text()
|
37
|
+
|
38
|
+
# iterate over all image layers
|
39
|
+
while parent_layer:
|
40
|
+
hash_type, layer_hash = parent_layer.split(":")
|
41
|
+
layer_ref = root.joinpath("image", "overlay2", "layerdb", hash_type, layer_hash)
|
42
|
+
cache_id = layer_ref.joinpath("cache-id").read_text()
|
43
|
+
layers.append(("/", root.joinpath("overlay2", cache_id, "diff")))
|
44
|
+
|
45
|
+
if (parent_file := layer_ref.joinpath("parent")).exists():
|
46
|
+
parent_layer = parent_file.read_text()
|
47
|
+
else:
|
48
|
+
parent_layer = None
|
49
|
+
|
50
|
+
# add the container layers
|
51
|
+
for container_layer_name in ["init-id", "mount-id"]:
|
52
|
+
layer = path.joinpath(container_layer_name).read_text()
|
53
|
+
layers.append(("/", root.joinpath("overlay2", layer, "diff")))
|
54
|
+
|
55
|
+
# add anonymous volumes, named volumes and bind mounts
|
56
|
+
if (config_path := root.joinpath("containers", path.name, "config.v2.json")).exists():
|
57
|
+
try:
|
58
|
+
config = json.loads(config_path.read_text())
|
59
|
+
except json.JSONDecodeError as e:
|
60
|
+
log.warning("Unable to parse overlay mounts for container %s", path.name)
|
61
|
+
log.debug("", exc_info=e)
|
62
|
+
return
|
63
|
+
|
64
|
+
for mount in config.get("MountPoints").values():
|
65
|
+
if not mount["Type"] in ["volume", "bind"]:
|
66
|
+
log.warning("Encountered unsupported mount type %s in container %s", mount["Type"], path.name)
|
67
|
+
continue
|
68
|
+
|
69
|
+
if not mount["Source"] and mount["Name"]:
|
70
|
+
# anonymous volumes do not have a Source but a volume id
|
71
|
+
layer = root.joinpath("volumes", mount["Name"], "_data")
|
72
|
+
elif mount["Source"]:
|
73
|
+
# named volumes and bind mounts have a Source set
|
74
|
+
layer = root.parents[-1].joinpath(mount["Source"])
|
75
|
+
else:
|
76
|
+
log.warning("Could not determine layer source for mount in container %s", path.name)
|
77
|
+
log.debug(json.dumps(mount))
|
78
|
+
continue
|
79
|
+
|
80
|
+
layers.append((mount["Destination"], layer))
|
81
|
+
|
82
|
+
# add hosts, hostname and resolv.conf files
|
83
|
+
for file in ["HostnamePath", "HostsPath", "ResolvConfPath"]:
|
84
|
+
if not config.get(file) or not (fp := path.parents[-1].joinpath(config.get(file))).exists():
|
85
|
+
log.warning("Container %s has no %s mount", path.name, file)
|
86
|
+
continue
|
87
|
+
|
88
|
+
layers.append(("/etc/" + fp.name, fp))
|
89
|
+
|
90
|
+
# append and mount every layer
|
91
|
+
for dest, layer in layers:
|
92
|
+
if layer.is_file() and dest in ["/etc/hosts", "/etc/hostname", "/etc/resolv.conf"]:
|
93
|
+
layer_fs = VirtualFilesystem()
|
94
|
+
layer_fs.map_file_fh("/etc/" + layer.name, layer.open("rb"))
|
95
|
+
dest = dest.split("/")[0]
|
96
|
+
|
97
|
+
else:
|
98
|
+
layer_fs = DirectoryFilesystem(layer)
|
99
|
+
|
100
|
+
self.append_layer().mount(dest, layer_fs)
|
101
|
+
|
102
|
+
def __repr__(self) -> str:
|
103
|
+
return f"<{self.__class__.__name__} {self.base_path}>"
|