dissect.target 3.19.dev57__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.dev57.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
  170. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
  171. {dissect.target-3.19.dev57.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.dev57.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
  178. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
  179. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
  180. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -29,6 +29,8 @@ def timezone_from_path(path: Path) -> str:
29
29
 
30
30
 
31
31
  class LocalePlugin(Plugin):
32
+ """Unix locale plugin."""
33
+
32
34
  def check_compatible(self) -> None:
33
35
  pass
34
36
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import BinaryIO, Iterable
3
+ from typing import BinaryIO, Iterable, Iterator
4
4
 
5
5
  from dissect.cstruct import cstruct
6
6
 
@@ -69,6 +69,8 @@ class GNULocateFile:
69
69
 
70
70
 
71
71
  class GNULocatePlugin(BaseLocatePlugin):
72
+ """GNU locate plugin."""
73
+
72
74
  __namespace__ = "gnulocate"
73
75
 
74
76
  path = "/var/cache/locate/locatedb"
@@ -78,7 +80,7 @@ class GNULocatePlugin(BaseLocatePlugin):
78
80
  raise UnsupportedPluginError(f"No locatedb file found at {self.path}")
79
81
 
80
82
  @export(record=GNULocateRecord)
81
- def locate(self) -> GNULocateRecord:
83
+ def locate(self) -> Iterator[GNULocateRecord]:
82
84
  """Yield file and directory names from GNU findutils' locatedb file.
83
85
 
84
86
  Resources:
@@ -115,6 +115,8 @@ class MLocateFile:
115
115
 
116
116
 
117
117
  class MLocatePlugin(BaseLocatePlugin):
118
+ """Unix mlocate plugin."""
119
+
118
120
  __namespace__ = "mlocate"
119
121
 
120
122
  path = "/var/lib/mlocate/mlocate.db"
@@ -163,6 +163,8 @@ class PLocateFile:
163
163
 
164
164
 
165
165
  class PLocatePlugin(BaseLocatePlugin):
166
+ """Unix plocate plugin."""
167
+
166
168
  __namespace__ = "plocate"
167
169
 
168
170
  path = "/var/lib/plocate/plocate.db"
@@ -177,7 +179,7 @@ class PLocatePlugin(BaseLocatePlugin):
177
179
  )
178
180
 
179
181
  @export(record=PLocateRecord)
180
- def locate(self) -> PLocateRecord:
182
+ def locate(self) -> Iterator[PLocateRecord]:
181
183
  """Yield file and directory names from the plocate.db.
182
184
 
183
185
  ``plocate`` is the default package on Ubuntu 22 and newer to locate files.
@@ -247,6 +247,8 @@ class AtopFile:
247
247
 
248
248
 
249
249
  class AtopPlugin(Plugin):
250
+ """Unix atop plugin."""
251
+
250
252
  ATOP_GLOB = "atop_*"
251
253
  ATOP_MAGIC = 0xFEEDBEEF
252
254
  ATOP_PATH = "/var/log/atop"
@@ -24,6 +24,8 @@ AUDIT_REGEX = re.compile(r"^type=(?P<audit_type>.*) msg=audit\((?P<ts>.*):(?P<au
24
24
 
25
25
 
26
26
  class AuditPlugin(Plugin):
27
+ """Unix audit log plugin."""
28
+
27
29
  def __init__(self, target):
28
30
  super().__init__(target)
29
31
  self.log_paths = self.get_log_paths()
@@ -51,7 +53,7 @@ class AuditPlugin(Plugin):
51
53
 
52
54
  return log_paths
53
55
 
54
- @export(record=[AuditRecord])
56
+ @export(record=AuditRecord)
55
57
  def audit(self) -> Iterator[AuditRecord]:
56
58
  """Return CentOS and RedHat audit information stored in /var/log/audit*.
57
59
 
@@ -1,60 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import logging
1
5
  import re
6
+ from abc import ABC, abstractmethod
7
+ from datetime import datetime
8
+ from functools import lru_cache
2
9
  from itertools import chain
3
- from typing import Iterator
10
+ from pathlib import Path
11
+ from typing import Any, Iterator
4
12
 
13
+ from dissect.target import Target
5
14
  from dissect.target.exceptions import UnsupportedPluginError
6
- from dissect.target.helpers.record import TargetRecordDescriptor
15
+ from dissect.target.helpers.fsutil import open_decompress
16
+ from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor
7
17
  from dissect.target.helpers.utils import year_rollover_helper
8
- from dissect.target.plugin import Plugin, export
9
-
10
- AuthLogRecord = TargetRecordDescriptor(
11
- "linux/log/auth",
12
- [
13
- ("datetime", "ts"),
14
- ("string", "message"),
15
- ("path", "source"),
16
- ],
18
+ from dissect.target.plugin import Plugin, alias, export
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+ RE_TS = re.compile(r"^[A-Za-z]{3}\s*\d{1,2}\s\d{1,2}:\d{2}:\d{2}")
23
+ RE_TS_ISO = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}")
24
+ RE_LINE = re.compile(
25
+ r"""
26
+ \d{2}:\d{2}\s # First match on the similar ending of the different timestamps
27
+ (?P<hostname>\S+)\s # The hostname
28
+ (?P<service>\S+?)(\[(?P<pid>\d+)\])?: # The service with optionally the PID between brackets
29
+ \s*(?P<message>.+?)\s*$ # The log message stripped from spaces left and right
30
+ """,
31
+ re.VERBOSE,
32
+ )
33
+
34
+ # Generic regular expressions
35
+ RE_IPV4_ADDRESS = re.compile(
36
+ r"""
37
+ ((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # First three octets
38
+ (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # Last octet
39
+ """,
40
+ re.VERBOSE,
17
41
  )
42
+ RE_USER = re.compile(r"for ([^\s]+)")
43
+
44
+
45
+ class BaseService(ABC):
46
+ @classmethod
47
+ @abstractmethod
48
+ def parse(cls, message: str) -> dict[str, any]:
49
+ pass
50
+
51
+
52
+ class SudoService(BaseService):
53
+ """Parsing of sudo service messages in the auth log."""
54
+
55
+ RE_SUDO_COMMAND = re.compile(
56
+ r"""
57
+ TTY=(?P<tty>\w+\/\w+)\s;\s # The TTY -> TTY=pts/0 ;
58
+ PWD=(?P<pwd>[\/\w]+)\s;\s # The current working directory -> PWD="/home/user" ;
59
+ USER=(?P<effective_user>\w+)\s;\s # The effective user -> USER=root ;
60
+ COMMAND=(?P<command>.+)$ # The command -> COMMAND=/usr/bin/whoami
61
+ """,
62
+ re.VERBOSE,
63
+ )
64
+
65
+ @classmethod
66
+ def parse(cls, message: str) -> dict[str, str]:
67
+ """Parse auth log message from sudo."""
68
+ if not (match := cls.RE_SUDO_COMMAND.search(message)):
69
+ return {}
70
+
71
+ additional_fields = {}
72
+ for key, value in match.groupdict().items():
73
+ additional_fields[key] = value
74
+
75
+ return additional_fields
76
+
77
+
78
+ class SshdService(BaseService):
79
+ """Class for parsing sshd messages in the auth log."""
80
+
81
+ RE_SSHD_PORTREGEX = re.compile(r"port\s(\d+)")
82
+ RE_USER = re.compile(r"for\s([^\s]+)")
83
+
84
+ @classmethod
85
+ def parse(cls, message: str) -> dict[str, str | int]:
86
+ """Parse message from sshd"""
87
+ additional_fields = {}
88
+ if ip_address := RE_IPV4_ADDRESS.search(message):
89
+ field_name = "host_ip" if "listening" in message else "remote_ip"
90
+ additional_fields[field_name] = ip_address.group(0)
91
+ if port := cls.RE_SSHD_PORTREGEX.search(message):
92
+ additional_fields["port"] = int(port.group(1))
93
+ if user := cls.RE_USER.search(message):
94
+ additional_fields["user"] = user.group(1)
95
+ # Accepted publickey for test_user from 8.8.8.8 IP port 12345 ssh2: RSA SHA256:123456789asdfghjklertzuio
96
+ if "Accepted publickey" in message:
97
+ ssh_protocol, encryption_algo, key_info = message.split()[-3:]
98
+ hash_algo, key_hash = key_info.split(":")
99
+ additional_fields["ssh_protocol"] = ssh_protocol.strip(":")
100
+ additional_fields["encryption_algorithm"] = encryption_algo
101
+ additional_fields["hash_algorithm"] = hash_algo
102
+ additional_fields["key_hash"] = key_hash
103
+ if (failed := "Failed" in message) or "Accepted" in message:
104
+ action_type = "failed" if failed else "accepted"
105
+ additional_fields["action"] = f"{action_type} authentication"
106
+ additional_fields["authentication_type"] = "password" if "password" in message else "publickey"
107
+
108
+ return additional_fields
109
+
110
+
111
+ class SystemdLogindService(BaseService):
112
+ """Class for parsing systemd-logind messages in the auth log."""
113
+
114
+ RE_SYSTEMD_LOGIND_WATCHING = re.compile(
115
+ r"""
116
+ (?P<action>Watching\ssystem\sbuttons)\s # Action is "Watching system buttons"
117
+ on\s(?P<device>[^\s]+)\s # The device the button is related to -> /dev/input/event0
118
+ \((?P<device_name>.*?)\) # The device (button) name -> (Power button)
119
+ """,
120
+ re.VERBOSE,
121
+ )
122
+
123
+ @classmethod
124
+ def parse(cls, message: str):
125
+ """Parse auth log message from systemd-logind."""
126
+ additional_fields = {}
127
+ # Example: Nov 14 07:14:09 ubuntu-1 systemd-logind[4]: Removed session 4.
128
+ if "Removed" in message:
129
+ additional_fields["action"] = "removed session"
130
+ additional_fields["session"] = message.split()[-1].strip(".")
131
+ elif "Watching" in message and (match := cls.RE_SYSTEMD_LOGIND_WATCHING.search(message)):
132
+ for key, value in match.groupdict().items():
133
+ additional_fields[key] = value
134
+ # Example: New session 4 of user sampleuser.
135
+ elif "New session" in message:
136
+ parts = message.removeprefix("New session ").split()
137
+ additional_fields["action"] = "new session"
138
+ additional_fields["session"] = parts[0]
139
+ additional_fields["user"] = parts[-1].strip(".")
140
+ # Example: Session 4 logged out. Waiting for processes to exit.
141
+ elif "logged out" in message:
142
+ session = message.removeprefix("Session ").split(maxsplit=1)[0]
143
+ additional_fields["action"] = "logged out session"
144
+ additional_fields["session"] = session
145
+ # Example: New seat seat0.
146
+ elif "New seat" in message:
147
+ seat = message.split()[-1].strip(".")
148
+ additional_fields["action"] = "new seat"
149
+ additional_fields["seat"] = seat
150
+
151
+ return additional_fields
152
+
153
+
154
+ class SuService(BaseService):
155
+ """Class for parsing su messages in the auth log."""
156
+
157
+ RE_SU_BY = re.compile(r"by\s([^\s]+)")
158
+ RE_SU_ON = re.compile(r"on\s([^\s]+)")
159
+ RE_SU_COMMAND = re.compile(r"'(.*?)'")
160
+
161
+ @classmethod
162
+ def parse(cls, message: str) -> dict[str, str]:
163
+ additional_fields = {}
164
+ if user := RE_USER.search(message):
165
+ additional_fields["user"] = user.group(1)
166
+ if by := cls.RE_SU_BY.search(message):
167
+ additional_fields["by"] = by.group(1)
168
+ if on := cls.RE_SU_ON.search(message):
169
+ additional_fields["device"] = on.group(1)
170
+ if command := cls.RE_SU_COMMAND.search(message):
171
+ additional_fields["command"] = command.group(1)
172
+ if (failed := "failed" in message) or "Successful" in message:
173
+ additional_fields["su_result"] = "failed" if failed else "success"
174
+
175
+ return additional_fields
176
+
177
+
178
+ class PkexecService(BaseService):
179
+ """Class for parsing pkexec messages in the auth log."""
180
+
181
+ RE_PKEXEC_COMMAND = re.compile(
182
+ r"""
183
+ (?P<user>\S+?):\sExecuting\scommand\s # Starts with actual user -> user:
184
+ \[USER=(?P<effective_user>[^\]]+)\]\s # The impersonated user -> [USER=root]
185
+ \[TTY=(?P<tty>[^\]]+)\]\s # The tty -> [TTY=unknown]
186
+ \[CWD=(?P<cwd>[^\]]+)\]\s # Current working directory -> [CWD=/home/user]
187
+ \[COMMAND=(?P<command>[^\]]+)\] # Command -> [COMMAND=/usr/lib/example]
188
+ """,
189
+ re.VERBOSE,
190
+ )
191
+
192
+ @classmethod
193
+ def parse(cls, message: str) -> dict[str, str]:
194
+ """Parse auth log message from pkexec"""
195
+ additional_fields = {}
196
+ if exec_cmd := cls.RE_PKEXEC_COMMAND.search(message):
197
+ additional_fields["action"] = "executing command"
198
+ for key, value in exec_cmd.groupdict().items():
199
+ if value and value.isdigit():
200
+ value = int(value)
201
+ additional_fields[key] = value
202
+
203
+ return additional_fields
18
204
 
19
- _TS_REGEX = r"^[A-Za-z]{3}\s*[0-9]{1,2}\s[0-9]{1,2}:[0-9]{2}:[0-9]{2}"
20
- RE_TS = re.compile(_TS_REGEX)
21
- RE_TS_AND_HOSTNAME = re.compile(_TS_REGEX + r"\s\S+\s")
205
+
206
+ class PamUnixService(BaseService):
207
+ RE_PAM_UNIX = re.compile(
208
+ r"""
209
+ pam_unix\([^\s]+:session\):\s(?P<action>session\s\w+)\s # Session action, usually opened or closed
210
+ for\suser\s(?P<user>[^\s\(]+)(?:\(uid=(?P<user_uid>\d+)\))? # User may contain uid like: root(uid=0)
211
+ (?:\sby\s\(uid=(?P<by_uid>\d+)\))?$ # Opened action also contains by
212
+ """,
213
+ re.VERBOSE,
214
+ )
215
+
216
+ @classmethod
217
+ def parse(cls, message):
218
+ """Parse auth log message from pluggable authentication modules (PAM)."""
219
+ if not (match := cls.RE_PAM_UNIX.search(message)):
220
+ return {}
221
+
222
+ additional_fields = {}
223
+ for key, value in match.groupdict().items():
224
+ if value and value.isdigit():
225
+ value = int(value)
226
+ additional_fields[key] = value
227
+
228
+ return additional_fields
229
+
230
+
231
+ class AuthLogRecordBuilder:
232
+ """Class for dynamically creating auth log records."""
233
+
234
+ RECORD_NAME = "linux/log/auth"
235
+ SERVICES: dict[str, BaseService] = {
236
+ "su": SuService,
237
+ "sudo": SudoService,
238
+ "sshd": SshdService,
239
+ "systemd-logind": SystemdLogindService,
240
+ "pkexec": PkexecService,
241
+ }
242
+
243
+ def __init__(self, target: Target):
244
+ self._create_event_descriptor = lru_cache(4096)(self._create_event_descriptor)
245
+ self.target = target
246
+
247
+ def _parse_additional_fields(self, service: str | None, message: str) -> dict[str, Any]:
248
+ """Parse additional fields in the message based on the service."""
249
+ if "pam_unix(" in message:
250
+ return PamUnixService.parse(message)
251
+
252
+ if service not in self.SERVICES:
253
+ self.target.log.debug("Service %s is not recognised, no additional fields could be parsed", service)
254
+ return {}
255
+
256
+ try:
257
+ return self.SERVICES[service].parse(message)
258
+ except Exception as e:
259
+ self.target.log.warning("Parsing additional fields in message '%s' for service %s failed", message, service)
260
+ self.target.log.debug("", exc_info=e)
261
+ raise e
262
+
263
+ def build_record(self, ts: datetime, source: Path, line: str) -> TargetRecordDescriptor:
264
+ """Builds an ``AuthLog`` event record."""
265
+
266
+ record_fields = [
267
+ ("datetime", "ts"),
268
+ ("path", "source"),
269
+ ("string", "service"),
270
+ ("varint", "pid"),
271
+ ("string", "message"),
272
+ ]
273
+
274
+ record_values = {
275
+ "ts": ts,
276
+ "message": line,
277
+ "service": None,
278
+ "pid": None,
279
+ "source": source,
280
+ "_target": self.target,
281
+ }
282
+
283
+ match = RE_LINE.search(line)
284
+ if match:
285
+ record_values.update(match.groupdict())
286
+
287
+ for key, value in self._parse_additional_fields(record_values["service"], line).items():
288
+ record_type = "string"
289
+ if isinstance(value, int):
290
+ record_type = "varint"
291
+
292
+ record_fields.append((record_type, key))
293
+ record_values[key] = value
294
+
295
+ # tuple conversion here is needed for lru_cache
296
+ desc = self._create_event_descriptor(tuple(record_fields))
297
+ return desc(**record_values)
298
+
299
+ def _create_event_descriptor(self, record_fields) -> TargetRecordDescriptor:
300
+ return TargetRecordDescriptor(self.RECORD_NAME, record_fields)
22
301
 
23
302
 
24
303
  class AuthPlugin(Plugin):
304
+ """Unix authentication log plugin."""
305
+
306
+ def __init__(self, target: Target):
307
+ super().__init__(target)
308
+ self._auth_log_builder = AuthLogRecordBuilder(target)
309
+
25
310
  def check_compatible(self) -> None:
26
311
  var_log = self.target.fs.path("/var/log")
27
312
  if not any(var_log.glob("auth.log*")) and not any(var_log.glob("secure*")):
28
313
  raise UnsupportedPluginError("No auth log files found")
29
314
 
30
- @export(record=[AuthLogRecord])
31
- def securelog(self) -> Iterator[AuthLogRecord]:
32
- """Return contents of /var/log/auth.log* and /var/log/secure*."""
33
- return self.authlog()
315
+ @alias("securelog")
316
+ @export(record=DynamicDescriptor(["datetime", "path", "string"]))
317
+ def authlog(self) -> Iterator[Any]:
318
+ """Yield contents of ``/var/log/auth.log*`` and ``/var/log/secure*`` files.
319
+
320
+ Order of returned events is not guaranteed to be chronological because of year
321
+ rollover detection efforts for log files without a year in the timestamp.
322
+
323
+ The following timestamp formats are recognised automatically. This plugin
324
+ assumes that no custom ``date_format`` template is set in ``syslog-ng`` or ``systemd``
325
+ configuration (defaults to ``M d H:M:S``).
326
+
327
+ ISO formatted authlog entries are parsed as can be found in Ubuntu 24.04 and later.
34
328
 
35
- @export(record=[AuthLogRecord])
36
- def authlog(self) -> Iterator[AuthLogRecord]:
37
- """Return contents of /var/log/auth.log* and /var/log/secure*."""
329
+ .. code-block:: text
38
330
 
39
- # Assuming no custom date_format template is set in syslog-ng or systemd (M d H:M:S)
40
- # CentOS format: Jan 12 13:37:00 hostname daemon: message
41
- # Debian format: Jan 12 13:37:00 hostname daemon[pid]: pam_unix(daemon:session): message
331
+ CentOS format: Jan 12 13:37:00 hostname daemon: message
332
+ Debian format: Jan 12 13:37:00 hostname daemon[pid]: pam_unix(daemon:session): message
333
+ Ubuntu 24.04: 2024-01-12T13:37:00.000000+02:00 hostname daemon[pid]: pam_unix(daemon:session): message
334
+
335
+ Resources:
336
+ - https://help.ubuntu.com/community/LinuxLogFiles
337
+ """
42
338
 
43
339
  tzinfo = self.target.datetime.tzinfo
44
340
 
45
341
  var_log = self.target.fs.path("/var/log")
46
342
  for auth_file in chain(var_log.glob("auth.log*"), var_log.glob("secure*")):
47
- for ts, line in year_rollover_helper(auth_file, RE_TS, "%b %d %H:%M:%S", tzinfo):
48
- ts_and_hostname = re.search(RE_TS_AND_HOSTNAME, line)
49
- if not ts_and_hostname:
50
- self.target.log.warning("No timstamp and hostname found on one of the lines in %s.", auth_file)
51
- self.target.log.debug("Skipping this line: %s", line)
52
- continue
53
-
54
- message = line.replace(ts_and_hostname.group(0), "").strip()
55
- yield AuthLogRecord(
56
- ts=ts,
57
- message=message,
58
- source=auth_file,
59
- _target=self.target,
60
- )
343
+ if is_iso_fmt(auth_file):
344
+ iterable = iso_readlines(auth_file)
345
+ else:
346
+ iterable = year_rollover_helper(auth_file, RE_TS, "%b %d %H:%M:%S", tzinfo)
347
+
348
+ for ts, line in iterable:
349
+ yield self._auth_log_builder.build_record(ts, auth_file, line)
350
+
351
+
352
+ def iso_readlines(file: Path) -> Iterator[tuple[datetime, str]]:
353
+ """Iterator reading the provided auth log file in ISO format. Mimics ``year_rollover_helper`` behaviour."""
354
+ with open_decompress(file, "rt") as fh:
355
+ for line in fh:
356
+ if not (match := RE_TS_ISO.match(line)):
357
+ log.warning("No timestamp found in one of the lines in %s!", file)
358
+ log.debug("Skipping line: %s", line)
359
+ continue
360
+
361
+ try:
362
+ ts = datetime.strptime(match[0], "%Y-%m-%dT%H:%M:%S.%f%z")
363
+ except ValueError as e:
364
+ log.warning("Unable to parse ISO timestamp in line: %s", line)
365
+ log.debug("", exc_info=e)
366
+ continue
367
+
368
+ yield ts, line
369
+
370
+
371
+ def is_iso_fmt(file: Path) -> bool:
372
+ """Determine if the provided auth log file uses new ISO format logging or not."""
373
+ return any(itertools.islice(iso_readlines(file), 0, 2))