dissect.target 3.20.dev45__py3-none-any.whl → 3.20.dev47__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,62 +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
18
202
 
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")
203
+ return additional_fields
204
+
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):
25
- """Unix auth log plugin."""
304
+ """Unix authentication log plugin."""
305
+
306
+ def __init__(self, target: Target):
307
+ super().__init__(target)
308
+ self._auth_log_builder = AuthLogRecordBuilder(target)
26
309
 
27
310
  def check_compatible(self) -> None:
28
311
  var_log = self.target.fs.path("/var/log")
29
312
  if not any(var_log.glob("auth.log*")) and not any(var_log.glob("secure*")):
30
313
  raise UnsupportedPluginError("No auth log files found")
31
314
 
32
- @export(record=AuthLogRecord)
33
- def securelog(self) -> Iterator[AuthLogRecord]:
34
- """Return contents of /var/log/auth.log* and /var/log/secure*."""
35
- 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.
36
328
 
37
- @export(record=AuthLogRecord)
38
- def authlog(self) -> Iterator[AuthLogRecord]:
39
- """Return contents of /var/log/auth.log* and /var/log/secure*."""
329
+ .. code-block:: text
40
330
 
41
- # Assuming no custom date_format template is set in syslog-ng or systemd (M d H:M:S)
42
- # CentOS format: Jan 12 13:37:00 hostname daemon: message
43
- # 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
+ """
44
338
 
45
339
  tzinfo = self.target.datetime.tzinfo
46
340
 
47
341
  var_log = self.target.fs.path("/var/log")
48
342
  for auth_file in chain(var_log.glob("auth.log*"), var_log.glob("secure*")):
49
- for ts, line in year_rollover_helper(auth_file, RE_TS, "%b %d %H:%M:%S", tzinfo):
50
- ts_and_hostname = re.search(RE_TS_AND_HOSTNAME, line)
51
- if not ts_and_hostname:
52
- self.target.log.warning("No timstamp and hostname found on one of the lines in %s.", auth_file)
53
- self.target.log.debug("Skipping this line: %s", line)
54
- continue
55
-
56
- message = line.replace(ts_and_hostname.group(0), "").strip()
57
- yield AuthLogRecord(
58
- ts=ts,
59
- message=message,
60
- source=auth_file,
61
- _target=self.target,
62
- )
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.20.dev45
3
+ Version: 3.20.dev47
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -268,7 +268,7 @@ dissect/target/plugins/os/unix/locate/plocate.py,sha256=0p7ibPPrDlQuJNjJbV4rU0fJ
268
268
  dissect/target/plugins/os/unix/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
269
269
  dissect/target/plugins/os/unix/log/atop.py,sha256=zjG5eKS-X0mpBXs-Sg2f7RfQvtjt0T8JcteNd9DB_ok,16361
270
270
  dissect/target/plugins/os/unix/log/audit.py,sha256=rZwxC90Q0FOB5BZxplTJwCTIp0hdVpaps1e3C1fRYaM,3754
271
- dissect/target/plugins/os/unix/log/auth.py,sha256=tYe54PGaJtLdBbM425_8VHJFvLqj6aqHuu6ZrQhqEKM,2417
271
+ dissect/target/plugins/os/unix/log/auth.py,sha256=9NJvlo7Vbsp_ENJFpKd04PH_sUuOy6ueSBwQqY0MtKo,14546
272
272
  dissect/target/plugins/os/unix/log/journal.py,sha256=xe8p8MM_95uYjFNzNSP5IsoIthJtxwFEDicYR42RYAI,17681
273
273
  dissect/target/plugins/os/unix/log/lastlog.py,sha256=Wr3-2n1-GwckN9mSx-yM55N6_L0PQyx6TGHoEvuc6c0,2515
274
274
  dissect/target/plugins/os/unix/log/messages.py,sha256=XtjZ0a2budgQm_K5JT3fMf7JcjuD0AelcD3zOFN2xpI,5732
@@ -378,10 +378,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
378
378
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
379
379
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
380
380
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
381
- dissect.target-3.20.dev45.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
382
- dissect.target-3.20.dev45.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
383
- dissect.target-3.20.dev45.dist-info/METADATA,sha256=h2ywkvsL6FJ-kD4V9jjhxVVxdXnDlucQJPtL5oAmG-c,12897
384
- dissect.target-3.20.dev45.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
385
- dissect.target-3.20.dev45.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
386
- dissect.target-3.20.dev45.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
387
- dissect.target-3.20.dev45.dist-info/RECORD,,
381
+ dissect.target-3.20.dev47.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
382
+ dissect.target-3.20.dev47.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
383
+ dissect.target-3.20.dev47.dist-info/METADATA,sha256=28qUe6AIKvwvWI58yI9VPiy1U86ihuVGMhzR-avNrtc,12897
384
+ dissect.target-3.20.dev47.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
385
+ dissect.target-3.20.dev47.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
386
+ dissect.target-3.20.dev47.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
387
+ dissect.target-3.20.dev47.dist-info/RECORD,,