dissect.target 3.16.dev45__py3-none-any.whl → 3.17__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. dissect/target/container.py +1 -0
  2. dissect/target/containers/fortifw.py +190 -0
  3. dissect/target/filesystem.py +192 -67
  4. dissect/target/filesystems/dir.py +14 -1
  5. dissect/target/filesystems/overlay.py +103 -0
  6. dissect/target/helpers/compat/path_common.py +19 -5
  7. dissect/target/helpers/configutil.py +30 -7
  8. dissect/target/helpers/network_managers.py +101 -73
  9. dissect/target/helpers/record_modifier.py +4 -1
  10. dissect/target/loader.py +3 -1
  11. dissect/target/loaders/dir.py +23 -5
  12. dissect/target/loaders/itunes.py +3 -3
  13. dissect/target/loaders/mqtt.py +309 -0
  14. dissect/target/loaders/overlay.py +31 -0
  15. dissect/target/loaders/target.py +12 -9
  16. dissect/target/loaders/vb.py +2 -2
  17. dissect/target/loaders/velociraptor.py +5 -4
  18. dissect/target/plugin.py +1 -1
  19. dissect/target/plugins/apps/browser/brave.py +10 -0
  20. dissect/target/plugins/apps/browser/browser.py +43 -0
  21. dissect/target/plugins/apps/browser/chrome.py +10 -0
  22. dissect/target/plugins/apps/browser/chromium.py +234 -12
  23. dissect/target/plugins/apps/browser/edge.py +10 -0
  24. dissect/target/plugins/apps/browser/firefox.py +512 -19
  25. dissect/target/plugins/apps/browser/iexplore.py +2 -2
  26. dissect/target/plugins/apps/container/docker.py +24 -4
  27. dissect/target/plugins/apps/ssh/openssh.py +4 -0
  28. dissect/target/plugins/apps/ssh/putty.py +45 -14
  29. dissect/target/plugins/apps/ssh/ssh.py +40 -0
  30. dissect/target/plugins/apps/vpn/openvpn.py +115 -93
  31. dissect/target/plugins/child/docker.py +24 -0
  32. dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
  33. dissect/target/plugins/filesystem/walkfs.py +2 -2
  34. dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
  35. dissect/target/plugins/os/unix/esxi/_os.py +2 -2
  36. dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
  37. dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
  38. dissect/target/plugins/os/unix/linux/services.py +1 -0
  39. dissect/target/plugins/os/unix/linux/sockets.py +2 -2
  40. dissect/target/plugins/os/unix/log/messages.py +53 -8
  41. dissect/target/plugins/os/windows/_os.py +10 -1
  42. dissect/target/plugins/os/windows/catroot.py +178 -63
  43. dissect/target/plugins/os/windows/credhist.py +210 -0
  44. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  45. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  46. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  47. dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
  48. dissect/target/plugins/os/windows/sam.py +10 -1
  49. dissect/target/target.py +1 -1
  50. dissect/target/tools/dump/run.py +23 -28
  51. dissect/target/tools/dump/state.py +11 -8
  52. dissect/target/tools/dump/utils.py +5 -4
  53. dissect/target/tools/query.py +3 -15
  54. dissect/target/tools/shell.py +48 -8
  55. dissect/target/tools/utils.py +23 -0
  56. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
  57. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/RECORD +62 -55
  58. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
  59. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
  60. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
  61. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
  62. {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -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 ``FilesystemEntry`` is a
529
- symbolic link or else the ``FilesystemEntry`` itself.
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, return them as FilesystemEntry's.
563
+ """Iterate over the contents of a directory, yields :class:`FilesystemEntry`.
564
564
 
565
565
  Returns:
566
- An iterator of directory entries as FilesystemEntry's.
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's.
579
+ """List the contents of a directory as a list of :class:`FilesystemEntry`.
580
580
 
581
581
  Returns:
582
- A list of FilesystemEntry's.
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's.
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 directory entries as FilesystemEntry's.
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 ``pattern`` as FilesysmteEntry's.
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's that match the pattern.
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 the FilesystemEntry it points to.
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 RootFilesystem(Filesystem):
1295
- __type__ = "root"
1296
+ class LayerFilesystem(Filesystem):
1297
+ __type__ = "layer"
1296
1298
 
1297
- def __init__(self, target: Target):
1298
- self.target = target
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 = RootFilesystemEntry(self, "/", [])
1304
- self.root = self.add_layer()
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 RootFilesystem class")
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.add_layer()
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 RootFilesystemEntry to another location."""
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 add_layer(self, **kwargs) -> VirtualFilesystem:
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.layers.append(layer)
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: FilesystemEntry = None) -> FilesystemEntry:
1366
- self.target.log.debug("%r::get(%r)", self, path)
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 RootFilesystemEntry(self, full_path, entries)
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
- Expose a getattr on a list of items. Useful in cases where
1409
- there's a virtual filesystem entry as well as a real one.
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: Any):
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 RootFilesystemEntry(FilesystemEntry):
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[FilesystemEntry]:
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 RootFilesystemEntry(selfentry.fs, path, entries)
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
- return os.readlink(self.entry) # Python 3.7 compatibility
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}>"