dissect.target 3.19.dev58__py3-none-any.whl → 3.20__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. dissect/target/container.py +1 -1
  2. dissect/target/exceptions.py +6 -5
  3. dissect/target/filesystem.py +2 -2
  4. dissect/target/filesystems/btrfs.py +14 -5
  5. dissect/target/filesystems/config.py +5 -1
  6. dissect/target/filesystems/extfs.py +5 -4
  7. dissect/target/filesystems/fat.py +22 -16
  8. dissect/target/filesystems/ffs.py +11 -4
  9. dissect/target/filesystems/jffs.py +12 -7
  10. dissect/target/filesystems/ntfs.py +22 -6
  11. dissect/target/filesystems/overlay.py +14 -4
  12. dissect/target/filesystems/smb.py +3 -3
  13. dissect/target/filesystems/squashfs.py +4 -4
  14. dissect/target/filesystems/vmfs.py +4 -4
  15. dissect/target/filesystems/xfs.py +15 -8
  16. dissect/target/helpers/compat/path_common.py +5 -5
  17. dissect/target/helpers/configutil.py +128 -32
  18. dissect/target/helpers/cyber.py +2 -0
  19. dissect/target/helpers/data/windowsZones.xml +19 -23
  20. dissect/target/helpers/docs.py +1 -1
  21. dissect/target/helpers/keychain.py +2 -0
  22. dissect/target/helpers/mount.py +2 -1
  23. dissect/target/helpers/record.py +29 -2
  24. dissect/target/helpers/record_modifier.py +5 -1
  25. dissect/target/helpers/regutil.py +56 -26
  26. dissect/target/loader.py +1 -1
  27. dissect/target/loaders/mqtt.py +104 -9
  28. dissect/target/loaders/proxmox.py +68 -0
  29. dissect/target/loaders/vma.py +1 -1
  30. dissect/target/loaders/xva.py +1 -1
  31. dissect/target/plugin.py +24 -21
  32. dissect/target/plugins/apps/av/mcafee.py +2 -0
  33. dissect/target/plugins/apps/av/sophos.py +2 -0
  34. dissect/target/plugins/apps/av/trendmicro.py +2 -0
  35. dissect/target/plugins/apps/browser/chromium.py +27 -6
  36. dissect/target/plugins/apps/container/docker.py +48 -32
  37. dissect/target/plugins/apps/editor/__init__.py +0 -0
  38. dissect/target/plugins/apps/editor/editor.py +23 -0
  39. dissect/target/plugins/apps/{texteditor → editor}/windowsnotepad.py +40 -31
  40. dissect/target/plugins/apps/other/__init__.py +0 -0
  41. dissect/target/plugins/apps/other/env.py +56 -0
  42. dissect/target/plugins/apps/shell/powershell.py +6 -2
  43. dissect/target/plugins/apps/shell/wget.py +91 -0
  44. dissect/target/plugins/apps/ssh/openssh.py +2 -0
  45. dissect/target/plugins/apps/ssh/opensshd.py +2 -0
  46. dissect/target/plugins/apps/virtualization/__init__.py +0 -0
  47. dissect/target/plugins/apps/virtualization/vmware_workstation.py +61 -0
  48. dissect/target/plugins/apps/vpn/wireguard.py +9 -9
  49. dissect/target/plugins/apps/webhosting/cpanel.py +2 -0
  50. dissect/target/plugins/apps/webserver/caddy.py +2 -0
  51. dissect/target/plugins/apps/webserver/nginx.py +2 -0
  52. dissect/target/plugins/child/esxi.py +3 -1
  53. dissect/target/plugins/child/parallels.py +68 -0
  54. dissect/target/plugins/child/proxmox.py +23 -0
  55. dissect/target/plugins/child/virtuozzo.py +12 -8
  56. dissect/target/plugins/child/vmware_workstation.py +23 -8
  57. dissect/target/plugins/filesystem/acquire_hash.py +2 -1
  58. dissect/target/plugins/filesystem/icat.py +15 -11
  59. dissect/target/plugins/filesystem/ntfs/mft.py +10 -6
  60. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +3 -1
  61. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -0
  62. dissect/target/plugins/filesystem/ntfs/utils.py +3 -1
  63. dissect/target/plugins/filesystem/unix/suid.py +4 -1
  64. dissect/target/plugins/filesystem/walkfs.py +2 -0
  65. dissect/target/plugins/general/example.py +2 -2
  66. dissect/target/plugins/general/loaders.py +18 -5
  67. dissect/target/plugins/general/network.py +20 -5
  68. dissect/target/plugins/general/osinfo.py +1 -0
  69. dissect/target/plugins/general/plugins.py +53 -10
  70. dissect/target/plugins/os/unix/_os.py +70 -44
  71. dissect/target/plugins/os/unix/applications.py +78 -0
  72. dissect/target/plugins/os/unix/bsd/citrix/history.py +2 -0
  73. dissect/target/plugins/os/unix/bsd/osx/_os.py +4 -21
  74. dissect/target/plugins/os/unix/bsd/osx/network.py +92 -0
  75. dissect/target/plugins/os/unix/bsd/osx/user.py +4 -0
  76. dissect/target/plugins/os/unix/cronjobs.py +8 -4
  77. dissect/target/plugins/os/unix/etc/etc.py +4 -0
  78. dissect/target/plugins/os/unix/generic.py +2 -0
  79. dissect/target/plugins/os/unix/history.py +27 -25
  80. dissect/target/plugins/os/unix/linux/_os.py +8 -10
  81. dissect/target/plugins/os/unix/linux/cmdline.py +2 -0
  82. dissect/target/plugins/os/unix/linux/debian/apt.py +4 -1
  83. dissect/target/plugins/os/unix/linux/debian/dpkg.py +3 -3
  84. dissect/target/plugins/os/unix/linux/debian/proxmox/__init__.py +0 -0
  85. dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +141 -0
  86. dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py +29 -0
  87. dissect/target/plugins/os/unix/linux/debian/snap.py +79 -0
  88. dissect/target/plugins/os/unix/linux/environ.py +2 -0
  89. dissect/target/plugins/os/unix/linux/fortios/_os.py +74 -63
  90. dissect/target/plugins/os/unix/linux/fortios/generic.py +2 -0
  91. dissect/target/plugins/os/unix/linux/fortios/locale.py +2 -0
  92. dissect/target/plugins/os/unix/linux/modules.py +2 -0
  93. dissect/target/plugins/os/unix/linux/netstat.py +2 -0
  94. dissect/target/{helpers → plugins/os/unix/linux}/network_managers.py +11 -9
  95. dissect/target/plugins/os/unix/linux/processes.py +2 -0
  96. dissect/target/plugins/os/unix/linux/redhat/yum.py +4 -1
  97. dissect/target/plugins/os/unix/linux/services.py +5 -3
  98. dissect/target/plugins/os/unix/linux/sockets.py +2 -0
  99. dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -1
  100. dissect/target/plugins/os/unix/locale.py +2 -0
  101. dissect/target/plugins/os/unix/locate/gnulocate.py +4 -2
  102. dissect/target/plugins/os/unix/locate/mlocate.py +2 -0
  103. dissect/target/plugins/os/unix/locate/plocate.py +3 -1
  104. dissect/target/plugins/os/unix/log/atop.py +2 -0
  105. dissect/target/plugins/os/unix/log/audit.py +3 -1
  106. dissect/target/plugins/os/unix/log/auth.py +351 -38
  107. dissect/target/plugins/os/unix/log/journal.py +123 -101
  108. dissect/target/plugins/os/unix/log/lastlog.py +5 -3
  109. dissect/target/plugins/os/unix/log/messages.py +51 -27
  110. dissect/target/plugins/os/unix/log/utmp.py +52 -71
  111. dissect/target/plugins/os/unix/packagemanager.py +5 -38
  112. dissect/target/plugins/os/unix/shadow.py +3 -1
  113. dissect/target/plugins/os/unix/trash.py +132 -0
  114. dissect/target/plugins/os/windows/_os.py +22 -41
  115. dissect/target/plugins/os/windows/activitiescache.py +9 -4
  116. dissect/target/plugins/os/windows/adpolicy.py +2 -1
  117. dissect/target/plugins/os/windows/amcache.py +16 -13
  118. dissect/target/plugins/os/windows/defender.py +4 -3
  119. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +3 -0
  120. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +3 -0
  121. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +3 -0
  122. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +3 -0
  123. dissect/target/plugins/os/windows/env.py +1 -2
  124. dissect/target/plugins/os/windows/exchange/exchange.py +6 -4
  125. dissect/target/plugins/os/windows/generic.py +68 -19
  126. dissect/target/plugins/os/windows/lnk.py +2 -0
  127. dissect/target/plugins/os/windows/locale.py +9 -3
  128. dissect/target/plugins/os/windows/log/etl.py +5 -4
  129. dissect/target/plugins/os/windows/log/evt.py +12 -8
  130. dissect/target/plugins/os/windows/log/evtx.py +9 -7
  131. dissect/target/plugins/os/windows/log/mssql.py +103 -0
  132. dissect/target/plugins/os/windows/log/pfro.py +2 -1
  133. dissect/target/plugins/os/windows/network.py +380 -0
  134. dissect/target/plugins/os/windows/notifications.py +6 -4
  135. dissect/target/plugins/os/windows/prefetch.py +7 -2
  136. dissect/target/plugins/os/windows/regf/7zip.py +9 -1
  137. dissect/target/plugins/os/windows/regf/applications.py +62 -0
  138. dissect/target/plugins/os/windows/regf/auditpol.py +2 -1
  139. dissect/target/plugins/os/windows/regf/bam.py +3 -1
  140. dissect/target/plugins/os/windows/regf/cit.py +14 -12
  141. dissect/target/plugins/os/windows/regf/clsid.py +6 -3
  142. dissect/target/plugins/os/windows/regf/firewall.py +2 -1
  143. dissect/target/plugins/os/windows/regf/mru.py +9 -8
  144. dissect/target/plugins/os/windows/regf/nethist.py +6 -3
  145. dissect/target/plugins/os/windows/regf/recentfilecache.py +3 -1
  146. dissect/target/plugins/os/windows/regf/regf.py +5 -1
  147. dissect/target/plugins/os/windows/regf/shellbags.py +351 -345
  148. dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
  149. dissect/target/plugins/os/windows/regf/usb.py +2 -1
  150. dissect/target/plugins/os/windows/regf/userassist.py +2 -1
  151. dissect/target/plugins/os/windows/registry.py +11 -0
  152. dissect/target/plugins/os/windows/services.py +3 -2
  153. dissect/target/plugins/os/windows/startupinfo.py +7 -2
  154. dissect/target/plugins/os/windows/syscache.py +5 -2
  155. dissect/target/plugins/os/windows/tasks.py +1 -1
  156. dissect/target/plugins/os/windows/thumbcache.py +11 -5
  157. dissect/target/plugins/os/windows/ual.py +12 -9
  158. dissect/target/plugins/os/windows/wer.py +21 -6
  159. dissect/target/plugins/os/windows/wua_history.py +0 -1
  160. dissect/target/target.py +13 -8
  161. dissect/target/tools/dump/utils.py +4 -0
  162. dissect/target/tools/fsutils.py +1 -1
  163. dissect/target/tools/info.py +1 -1
  164. dissect/target/tools/mount.py +15 -5
  165. dissect/target/tools/query.py +15 -9
  166. dissect/target/tools/shell.py +98 -9
  167. dissect/target/tools/utils.py +7 -7
  168. dissect/target/volume.py +4 -4
  169. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
  170. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
  171. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/WHEEL +1 -1
  172. dissect/target/helpers/targetd.py +0 -58
  173. dissect/target/loaders/targetd.py +0 -223
  174. dissect/target/plugins/apps/texteditor/texteditor.py +0 -13
  175. dissect/target/plugins/os/unix/etc.py +0 -9
  176. /dissect/target/plugins/apps/{texteditor → database}/__init__.py +0 -0
  177. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
  178. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
  179. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
  180. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  import gzip
2
2
  from datetime import datetime
3
- from typing import Dict, Generator, List, TextIO
3
+ from typing import Dict, Generator, Iterator, List, TextIO
4
4
 
5
5
  from dissect.target.exceptions import UnsupportedPluginError
6
6
  from dissect.target.helpers.record import TargetRecordDescriptor
@@ -59,7 +59,7 @@ class DpkgPlugin(Plugin):
59
59
  raise UnsupportedPluginError("No DPKG files found")
60
60
 
61
61
  @export(record=DpkgPackageStatusRecord)
62
- def status(self):
62
+ def status(self) -> Iterator[DpkgPackageStatusRecord]:
63
63
  """Yield records for packages in dpkg's status database"""
64
64
 
65
65
  status_file_path = self.target.fs.path(STATUS_FILE_NAME)
@@ -82,7 +82,7 @@ class DpkgPlugin(Plugin):
82
82
  yield DpkgPackageStatusRecord(_target=self.target, **record_fields)
83
83
 
84
84
  @export(record=DpkgPackageLogRecord)
85
- def log(self):
85
+ def log(self) -> Iterator[DpkgPackageLogRecord]:
86
86
  """Yield records for actions logged in dpkg's logs"""
87
87
 
88
88
  for log_file in self.target.fs.glob(LOG_FILES_GLOB):
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import stat
4
+ from io import BytesIO
5
+ from typing import BinaryIO
6
+
7
+ from dissect.sql import sqlite3
8
+ from dissect.util.stream import BufferedStream
9
+
10
+ from dissect.target.filesystem import (
11
+ Filesystem,
12
+ VirtualDirectory,
13
+ VirtualFile,
14
+ VirtualFilesystem,
15
+ )
16
+ from dissect.target.helpers import fsutil
17
+ from dissect.target.plugins.os.unix._os import OperatingSystem, export
18
+ from dissect.target.plugins.os.unix.linux.debian._os import DebianPlugin
19
+ from dissect.target.target import Target
20
+
21
+
22
+ class ProxmoxPlugin(DebianPlugin):
23
+ @classmethod
24
+ def detect(cls, target: Target) -> Filesystem | None:
25
+ for fs in target.filesystems:
26
+ if fs.exists("/etc/pve") or fs.exists("/var/lib/pve"):
27
+ return fs
28
+
29
+ return None
30
+
31
+ @classmethod
32
+ def create(cls, target: Target, sysvol: Filesystem) -> ProxmoxPlugin:
33
+ obj = super().create(target, sysvol)
34
+
35
+ if (config_db := target.fs.path("/var/lib/pve-cluster/config.db")).exists():
36
+ with config_db.open("rb") as fh:
37
+ vfs = _create_pmxcfs(fh, obj.hostname)
38
+
39
+ target.fs.mount("/etc/pve", vfs)
40
+
41
+ return obj
42
+
43
+ @export(property=True)
44
+ def version(self) -> str:
45
+ """Returns Proxmox VE version with underlying OS release."""
46
+
47
+ for pkg in self.target.dpkg.status():
48
+ if pkg.name == "proxmox-ve":
49
+ distro_name = self._os_release.get("PRETTY_NAME", "")
50
+ return f"{pkg.name} {pkg.version} ({distro_name})"
51
+
52
+ @export(property=True)
53
+ def os(self) -> str:
54
+ return OperatingSystem.PROXMOX.value
55
+
56
+
57
+ DT_DIR = 4
58
+ DT_REG = 8
59
+
60
+
61
+ def _create_pmxcfs(fh: BinaryIO, hostname: str | None = None) -> VirtualFilesystem:
62
+ # https://pve.proxmox.com/wiki/Proxmox_Cluster_File_System_(pmxcfs)
63
+ db = sqlite3.SQLite3(fh)
64
+
65
+ entries = {row.inode: row for row in db.table("tree")}
66
+
67
+ vfs = VirtualFilesystem()
68
+ for entry in entries.values():
69
+ if entry.type == DT_DIR:
70
+ cls = ProxmoxConfigDirectoryEntry
71
+ elif entry.type == DT_REG:
72
+ cls = ProxmoxConfigFileEntry
73
+ else:
74
+ raise ValueError(f"Unknown pmxcfs file type: {entry.type}")
75
+
76
+ parts = []
77
+ current = entry
78
+ while current.parent != 0:
79
+ parts.append(current.name)
80
+ current = entries[current.parent]
81
+ parts.append(current.name)
82
+
83
+ path = "/".join(parts[::-1])
84
+ vfs.map_file_entry(path, cls(vfs, path, entry))
85
+
86
+ if hostname:
87
+ node_root = vfs.path(f"nodes/{hostname}")
88
+ vfs.symlink(str(node_root), "local")
89
+ vfs.symlink(str(node_root / "lxc"), "lxc")
90
+ vfs.symlink(str(node_root / "openvz"), "openvz")
91
+ vfs.symlink(str(node_root / "qemu-server"), "qemu-server")
92
+
93
+ # TODO: .version, .members, .vmlist, maybe .clusterlog and .rrd?
94
+
95
+ return vfs
96
+
97
+
98
+ class ProxmoxConfigFileEntry(VirtualFile):
99
+ def open(self) -> BinaryIO:
100
+ return BufferedStream(BytesIO(self.entry.data or b""))
101
+
102
+ def lstat(self) -> fsutil.stat_result:
103
+ # ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
104
+ return fsutil.stat_result(
105
+ [
106
+ stat.S_IFREG | 0o640,
107
+ self.entry.inode,
108
+ id(self.fs),
109
+ 1,
110
+ 0,
111
+ 0,
112
+ len(self.entry.data) if self.entry.data else 0,
113
+ 0,
114
+ self.entry.mtime,
115
+ 0,
116
+ ]
117
+ )
118
+
119
+
120
+ class ProxmoxConfigDirectoryEntry(VirtualDirectory):
121
+ def __init__(self, fs: VirtualFilesystem, path: str, entry: sqlite3.Row):
122
+ super().__init__(fs, path)
123
+ self.entry = entry
124
+
125
+ def lstat(self) -> fsutil.stat_result:
126
+ """Return the stat information of the given path, without resolving links."""
127
+ # ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime']
128
+ return fsutil.stat_result(
129
+ [
130
+ stat.S_IFDIR | 0o755,
131
+ self.entry.inode,
132
+ id(self.fs),
133
+ 1,
134
+ 0,
135
+ 0,
136
+ 0,
137
+ 0,
138
+ self.entry.mtime,
139
+ 0,
140
+ ]
141
+ )
@@ -0,0 +1,29 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers.record import TargetRecordDescriptor
5
+ from dissect.target.plugin import Plugin, export
6
+
7
+ VirtualMachineRecord = TargetRecordDescriptor(
8
+ "proxmox/vm",
9
+ [
10
+ ("string", "path"),
11
+ ],
12
+ )
13
+
14
+
15
+ class VirtualMachinePlugin(Plugin):
16
+ """Plugin to list Proxmox virtual machines."""
17
+
18
+ def check_compatible(self) -> None:
19
+ if self.target.os != "proxmox":
20
+ raise UnsupportedPluginError("Not a Proxmox operating system")
21
+
22
+ @export(record=VirtualMachineRecord)
23
+ def vmlist(self) -> Iterator[VirtualMachineRecord]:
24
+ """List Proxmox virtual machines on this node."""
25
+ for config in self.target.fs.path("/etc/pve/qemu-server").iterdir():
26
+ yield VirtualMachineRecord(
27
+ path=config,
28
+ _target=self.target,
29
+ )
@@ -0,0 +1,79 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.filesystems.squashfs import SquashFSFilesystem
5
+ from dissect.target.helpers import configutil
6
+ from dissect.target.helpers.fsutil import TargetPath
7
+ from dissect.target.helpers.record import UnixApplicationRecord
8
+ from dissect.target.plugin import Plugin, alias, export
9
+ from dissect.target.target import Target
10
+
11
+
12
+ class SnapPlugin(Plugin):
13
+ """Canonical Linux Snapcraft plugin."""
14
+
15
+ PATHS = [
16
+ "/var/lib/snapd/snaps",
17
+ ]
18
+
19
+ def __init__(self, target: Target):
20
+ super().__init__(target)
21
+ self.installs = list(self._find_installs())
22
+
23
+ def check_compatible(self) -> None:
24
+ if not configutil.HAS_YAML:
25
+ raise UnsupportedPluginError("Missing required dependency ruamel.yaml")
26
+
27
+ if not self.installs:
28
+ raise UnsupportedPluginError("No snapd install folder(s) found")
29
+
30
+ def _find_installs(self) -> Iterator[TargetPath]:
31
+ for str_path in self.PATHS:
32
+ if (path := self.target.fs.path(str_path)).exists():
33
+ yield path
34
+
35
+ @export(record=UnixApplicationRecord)
36
+ @alias("snaps")
37
+ def snap(self) -> Iterator[UnixApplicationRecord]:
38
+ """Yields installed Canonical Linux Snapcraft (snaps) applications on the target system.
39
+
40
+ Reads information from installed SquashFS ``*.snap`` files found in ``/var/lib/snapd/snaps``.
41
+ Logs of the ``snapd`` daemon can be parsed using the ``journal`` or ``syslog`` plugins.
42
+
43
+ Resources:
44
+ - https://github.com/canonical/snapcraft
45
+ - https://en.wikipedia.org/wiki/Snap_(software)
46
+
47
+ Yields ``UnixApplicationRecord`` records with the following fields:
48
+
49
+ .. code-block:: text
50
+
51
+ ts_modified (datetime): timestamp when the installation was modified
52
+ name (string): name of the application
53
+ version (string): version of the application
54
+ path (string): path to the application snap file
55
+ """
56
+
57
+ for install_path in self.installs:
58
+ for snap in install_path.glob("*.snap"):
59
+ try:
60
+ squashfs = SquashFSFilesystem(snap.open())
61
+
62
+ except (ValueError, NotImplementedError) as e:
63
+ self.target.log.warning("Unable to open snap file %s", snap)
64
+ self.target.log.debug("", exc_info=e)
65
+ continue
66
+
67
+ if not (meta := squashfs.path("meta/snap.yaml")).exists():
68
+ self.target.log.warning("Snap %s has no meta/snap.yaml file")
69
+ continue
70
+
71
+ meta_data = configutil.parse(meta, hint="yaml")
72
+
73
+ yield UnixApplicationRecord(
74
+ ts_modified=meta.lstat().st_mtime,
75
+ name=meta_data.get("name"),
76
+ version=meta_data.get("version"),
77
+ path=snap,
78
+ _target=self.target,
79
+ )
@@ -16,6 +16,8 @@ EnvironmentVariableRecord = TargetRecordDescriptor(
16
16
 
17
17
 
18
18
  class EnvironPlugin(Plugin):
19
+ """Linux volatile proc environment plugin."""
20
+
19
21
  def check_compatible(self) -> None:
20
22
  self.target.proc
21
23
 
@@ -6,7 +6,7 @@ from base64 import b64decode
6
6
  from datetime import datetime
7
7
  from io import BytesIO
8
8
  from tarfile import ReadError
9
- from typing import BinaryIO, Iterator, Optional, TextIO, Union
9
+ from typing import BinaryIO, Iterator, TextIO
10
10
 
11
11
  from dissect.util import cpio
12
12
  from dissect.util.compression import xz
@@ -73,10 +73,11 @@ class FortiOSPlugin(LinuxPlugin):
73
73
  return config
74
74
 
75
75
  @classmethod
76
- def detect(cls, target: Target) -> Optional[Filesystem]:
76
+ def detect(cls, target: Target) -> Filesystem | None:
77
77
  for fs in target.filesystems:
78
- # Tested on FortiGate and FortiAnalyzer, other Fortinet devices may look different.
79
- if fs.exists("/rootfs.gz") and (fs.exists("/.fgtsum") or fs.exists("/.fmg_sign") or fs.exists("/flatkc")):
78
+ # Tested on FortiGate, FortiAnalyzer and FortiManager.
79
+ # Other Fortinet devices may look different.
80
+ if fs.exists("/rootfs.gz") and (any(map(fs.exists, (".fgtsum", ".fmg_sign", "flatkc", "system.conf")))):
80
81
  return fs
81
82
 
82
83
  @classmethod
@@ -212,7 +213,7 @@ class FortiOSPlugin(LinuxPlugin):
212
213
  return "FortiOS Unknown"
213
214
 
214
215
  @export(record=FortiOSUserRecord)
215
- def users(self) -> Iterator[Union[FortiOSUserRecord, UnixUserRecord]]:
216
+ def users(self) -> Iterator[FortiOSUserRecord | UnixUserRecord]:
216
217
  """Return local users of the FortiOS system."""
217
218
 
218
219
  # Possible unix-like users
@@ -224,7 +225,7 @@ class FortiOSPlugin(LinuxPlugin):
224
225
  yield FortiOSUserRecord(
225
226
  name=username,
226
227
  password=":".join(entry.get("password", [])),
227
- groups=[entry["accprofile"][0]],
228
+ groups=list(entry.get("accprofile", [])),
228
229
  home="/root",
229
230
  _target=self.target,
230
231
  )
@@ -233,69 +234,79 @@ class FortiOSPlugin(LinuxPlugin):
233
234
  self.target.log.debug("", exc_info=e)
234
235
 
235
236
  # FortiManager administrative users
236
- try:
237
- for username, entry in self._config["global-config"]["system"]["admin"]["user"].items():
238
- yield FortiOSUserRecord(
239
- name=username,
240
- password=":".join(entry.get("password", [])),
241
- groups=[entry["profileid"][0]],
242
- home="/root",
243
- _target=self.target,
244
- )
245
- except KeyError as e:
246
- self.target.log.warning("Exception while parsing FortiManager admin users")
247
- self.target.log.debug("", exc_info=e)
248
-
249
- # Local users
250
- try:
251
- local_groups = local_groups_to_users(self._config["root-config"]["user"]["group"])
252
- for username, entry in self._config["root-config"]["user"].get("local", {}).items():
253
- try:
254
- password = decrypt_password(entry["passwd"][-1])
255
- except (ValueError, RuntimeError):
256
- password = ":".join(entry.get("passwd", []))
257
-
258
- yield FortiOSUserRecord(
259
- name=username,
260
- password=password,
261
- groups=local_groups.get(username, []),
262
- home=None,
263
- _target=self.target,
264
- )
265
- except KeyError as e:
266
- self.target.log.warning("Exception while parsing FortiOS local users")
267
- self.target.log.debug("", exc_info=e)
268
-
269
- # Temporary guest users
270
- try:
271
- for _, entry in self._config["root-config"]["user"]["group"].get("guestgroup", {}).get("guest", {}).items():
272
- try:
273
- password = decrypt_password(entry.get("password")[-1])
274
- except (ValueError, RuntimeError):
275
- password = ":".join(entry.get("password"))
237
+ if self._config.get("global-config", {}).get("system", {}).get("admin", {}).get("user"):
238
+ try:
239
+ for username, entry in self._config["global-config"]["system"]["admin"]["user"].items():
240
+ yield FortiOSUserRecord(
241
+ name=username,
242
+ password=":".join(entry.get("password", [])),
243
+ groups=list(entry.get("profileid", [])),
244
+ home="/root",
245
+ _target=self.target,
246
+ )
247
+ except KeyError as e:
248
+ self.target.log.warning("Exception while parsing FortiManager admin users")
249
+ self.target.log.debug("", exc_info=e)
250
+
251
+ if self._config.get("root-config", {}).get("user", {}).get("local"):
252
+ # Local users
253
+ try:
254
+ local_groups = local_groups_to_users(self._config["root-config"]["user"]["group"])
255
+ except KeyError as e:
256
+ self.target.log.warning("Unable to get local user groups in root config")
257
+ self.target.log.debug("", exc_info=e)
258
+ local_groups = {}
276
259
 
277
- yield FortiOSUserRecord(
278
- name=entry["user-id"][0],
279
- password=password,
280
- groups=["guestgroup"],
281
- home=None,
282
- _target=self.target,
283
- )
284
- except KeyError as e:
285
- self.target.log.warning("Exception while parsing FortiOS temporary guest users")
286
- self.target.log.debug("", exc_info=e)
260
+ try:
261
+ for username, entry in self._config["root-config"]["user"].get("local", {}).items():
262
+ try:
263
+ password = decrypt_password(entry["passwd"][-1])
264
+ except (ValueError, RuntimeError):
265
+ password = ":".join(entry.get("passwd", []))
266
+
267
+ yield FortiOSUserRecord(
268
+ name=username,
269
+ password=password,
270
+ groups=local_groups.get(username, []),
271
+ home=None,
272
+ _target=self.target,
273
+ )
274
+ except KeyError as e:
275
+ self.target.log.warning("Exception while parsing FortiOS local users")
276
+ self.target.log.debug("", exc_info=e)
277
+
278
+ if self._config.get("root-config", {}).get("user", {}).get("group", {}).get("guestgroup"):
279
+ # Temporary guest users
280
+ try:
281
+ for _, entry in (
282
+ self._config["root-config"]["user"]["group"].get("guestgroup", {}).get("guest", {}).items()
283
+ ):
284
+ try:
285
+ password = decrypt_password(entry.get("password")[-1])
286
+ except (ValueError, RuntimeError):
287
+ password = ":".join(entry.get("password"))
288
+
289
+ yield FortiOSUserRecord(
290
+ name=entry["user-id"][0],
291
+ password=password,
292
+ groups=["guestgroup"],
293
+ home=None,
294
+ _target=self.target,
295
+ )
296
+ except KeyError as e:
297
+ self.target.log.warning("Exception while parsing FortiOS temporary guest users")
298
+ self.target.log.debug("", exc_info=e)
287
299
 
288
300
  @export(property=True)
289
301
  def os(self) -> str:
290
302
  return OperatingSystem.FORTIOS.value
291
303
 
292
304
  @export(property=True)
293
- def architecture(self) -> Optional[str]:
305
+ def architecture(self) -> str | None:
294
306
  """Return architecture FortiOS runs on."""
295
- paths = ["/lib/libav.so", "/bin/ctr"]
296
- for path in paths:
297
- if self.target.fs.path(path).exists():
298
- return self._get_architecture(path=path)
307
+ for path in ["/lib/libav.so", "/bin/ctr", "/bin/grep"]:
308
+ if (bin := self.target.fs.path(path)).exists():
309
+ return self._get_architecture(path=bin)
299
310
 
300
311
 
301
312
  class ConfigNode(dict):
@@ -528,7 +539,7 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
528
539
  return BytesIO(result)
529
540
 
530
541
 
531
- def _kdf_7_4_x(key_data: Union[str, bytes]) -> tuple[bytes, bytes]:
542
+ def _kdf_7_4_x(key_data: str | bytes) -> tuple[bytes, bytes]:
532
543
  """Derive 32 byte key and 16 byte IV from 32 byte seed.
533
544
 
534
545
  As the IV needs to be 16 bytes, we return the first 16 bytes of the sha256 hash.
@@ -542,7 +553,7 @@ def _kdf_7_4_x(key_data: Union[str, bytes]) -> tuple[bytes, bytes]:
542
553
  return key, iv
543
554
 
544
555
 
545
- def get_kernel_hash(sysvol: Filesystem) -> Optional[str]:
556
+ def get_kernel_hash(sysvol: Filesystem) -> str | None:
546
557
  """Return the SHA256 hash of the (compressed) kernel."""
547
558
  kernel_files = ["flatkc", "vmlinuz", "vmlinux"]
548
559
  for k in kernel_files:
@@ -9,6 +9,8 @@ from dissect.target.plugins.os.unix.generic import calculate_last_activity
9
9
 
10
10
 
11
11
  class GenericPlugin(Plugin):
12
+ """Generic FortiOS plugin."""
13
+
12
14
  def check_compatible(self) -> None:
13
15
  if self.target.os != "fortios":
14
16
  raise UnsupportedPluginError("FortiOS specific plugin loaded on non-FortiOS target")
@@ -5,6 +5,8 @@ from dissect.target.plugin import Plugin, export
5
5
 
6
6
 
7
7
  class LocalePlugin(Plugin):
8
+ """FortiOS locale plugin."""
9
+
8
10
  def check_compatible(self) -> None:
9
11
  if self.target.os != "fortios":
10
12
  raise UnsupportedPluginError("FortiOS specific plugin loaded on non-FortiOS target")
@@ -28,6 +28,8 @@ class Module:
28
28
 
29
29
 
30
30
  class ModulePlugin(Plugin):
31
+ """Linux volatile kernel ``/sys/module`` plugin."""
32
+
31
33
  def __init__(self, target: Target):
32
34
  super().__init__(target)
33
35
  self._module_base_path = self.target.fs.path("/sys/module")
@@ -8,6 +8,8 @@ NETSTAT_TEMPLATE = "{protocol:<12}{receive_queue:<10}{transmit_queue:<11}{local_
8
8
 
9
9
 
10
10
  class NetstatPlugin(Plugin):
11
+ """Linux volatile netstat plugin."""
12
+
11
13
  def check_compatible(self) -> None:
12
14
  self.target.proc
13
15
 
@@ -7,15 +7,17 @@ from configparser import ConfigParser, MissingSectionHeaderError
7
7
  from io import StringIO
8
8
  from itertools import chain
9
9
  from re import compile, sub
10
- from typing import Any, Callable, Iterable, Iterator, Match, Optional
10
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Match
11
11
 
12
12
  from defusedxml import ElementTree
13
13
 
14
14
  from dissect.target.exceptions import PluginError
15
- from dissect.target.helpers.fsutil import TargetPath
16
- from dissect.target.plugins.os.unix.log.journal import JournalRecord
17
- from dissect.target.plugins.os.unix.log.messages import MessagesRecord
18
- from dissect.target.target import Target
15
+
16
+ if TYPE_CHECKING:
17
+ from dissect.target.helpers.fsutil import TargetPath
18
+ from dissect.target.plugins.os.unix.log.journal import JournalRecord
19
+ from dissect.target.plugins.os.unix.log.messages import MessagesRecord
20
+ from dissect.target.target import Target
19
21
 
20
22
  log = logging.getLogger(__name__)
21
23
 
@@ -57,7 +59,7 @@ class Template:
57
59
  """Sets the name of the the used parsing template to the name of the discovered network manager."""
58
60
  self.name = name
59
61
 
60
- def create_config(self, path: TargetPath) -> Optional[dict]:
62
+ def create_config(self, path: TargetPath) -> dict | None:
61
63
  """Create a network config dictionary based on the configured template and supplied path.
62
64
 
63
65
  Args:
@@ -81,7 +83,7 @@ class Template:
81
83
  config = self._parse_configparser_config(path)
82
84
  return config
83
85
 
84
- def _parse_netplan_config(self, path: TargetPath) -> Optional[dict]:
86
+ def _parse_netplan_config(self, path: TargetPath) -> dict | None:
85
87
  """Internal function to parse a netplan YAML based configuration file into a dict.
86
88
 
87
89
  Args:
@@ -286,7 +288,7 @@ class Parser:
286
288
  if option in translation_values and value:
287
289
  return translation_key
288
290
 
289
- def _get_option(self, config: dict, option: str, section: Optional[str] = None) -> Optional[str | Callable]:
291
+ def _get_option(self, config: dict, option: str, section: str | None = None) -> str | Callable | None:
290
292
  """Internal function to get arbitrary options values from a parsed (non-translated) dictionary.
291
293
 
292
294
  Args:
@@ -334,7 +336,7 @@ class NetworkManager:
334
336
  self.config_globs = (config_globs,) if isinstance(config_globs, str) else config_globs
335
337
  self.detection_globs = (detection_globs,) if isinstance(detection_globs, str) else detection_globs
336
338
 
337
- def detect(self, target: Optional[Target] = None) -> bool:
339
+ def detect(self, target: Target | None = None) -> bool:
338
340
  """Detects if the network manager is active on the target
339
341
 
340
342
  Returns:
@@ -18,6 +18,8 @@ ProcProcessRecord = TargetRecordDescriptor(
18
18
 
19
19
 
20
20
  class ProcProcesses(Plugin):
21
+ """Linux ``/proc`` process volatile plugin."""
22
+
21
23
  def check_compatible(self) -> None:
22
24
  self.target.proc
23
25
 
@@ -7,13 +7,16 @@ from dissect.target.helpers.utils import year_rollover_helper
7
7
  from dissect.target.plugins.os.unix.packagemanager import (
8
8
  OperationTypes,
9
9
  PackageManagerLogRecord,
10
+ PackageManagerPlugin,
10
11
  )
11
12
 
12
13
  YUM_LOG_KEYWORDS = ["Installed", "Updated", "Erased", "Obsoleted"]
13
14
  RE_TS = re.compile(r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})")
14
15
 
15
16
 
16
- class YumPlugin(plugin.Plugin):
17
+ class YumPlugin(PackageManagerPlugin):
18
+ """Yum package manager plugin."""
19
+
17
20
  __namespace__ = "yum"
18
21
 
19
22
  LOG_DIR_PATH = "/var/log"
@@ -18,6 +18,8 @@ LinuxServiceRecord = TargetRecordDescriptor(RECORD_NAME, DEFAULT_ELEMENTS)
18
18
 
19
19
 
20
20
  class ServicesPlugin(Plugin):
21
+ """Linux services plugin."""
22
+
21
23
  SYSTEMD_PATHS = [
22
24
  "/etc/systemd/system",
23
25
  "/lib/systemd/system",
@@ -35,9 +37,9 @@ class ServicesPlugin(Plugin):
35
37
  """Return information about all installed systemd and init.d services.
36
38
 
37
39
  References:
38
- - https://geeksforgeeks.org/what-is-init-d-in-linux-service-management
39
- - http://0pointer.de/blog/projects/systemd-for-admins-3.html
40
- - https://www.freedesktop.org/software/systemd/man/systemd.syntax.html
40
+ - https://geeksforgeeks.org/what-is-init-d-in-linux-service-management
41
+ - http://0pointer.de/blog/projects/systemd-for-admins-3.html
42
+ - https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html
41
43
  """
42
44
 
43
45
  return chain(self.systemd(), self.initd())
@@ -64,6 +64,8 @@ PacketSocketRecord = TargetRecordDescriptor(
64
64
 
65
65
 
66
66
  class NetSocketPlugin(Plugin):
67
+ """Linux volatile net sockets plugin."""
68
+
67
69
  __namespace__ = "sockets"
68
70
 
69
71
  def __init__(self, target: Target):
@@ -7,10 +7,13 @@ from dissect.target.helpers.fsutil import open_decompress
7
7
  from dissect.target.plugins.os.unix.packagemanager import (
8
8
  OperationTypes,
9
9
  PackageManagerLogRecord,
10
+ PackageManagerPlugin,
10
11
  )
11
12
 
12
13
 
13
- class ZypperPlugin(plugin.Plugin):
14
+ class ZypperPlugin(PackageManagerPlugin):
15
+ """Zypper package manager plugin."""
16
+
14
17
  __namespace__ = "zypper"
15
18
 
16
19
  LOG_DIR_PATH = "/var/log/zypp"