dissect.target 3.20.dev45__py3-none-any.whl → 3.20.dev48__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,17 +1,17 @@
1
- import gzip
1
+ from __future__ import annotations
2
+
2
3
  import ipaddress
3
4
  import struct
4
5
  from collections import namedtuple
5
6
  from typing import Iterator
6
7
 
7
8
  from dissect.cstruct import cstruct
8
- from dissect.util.stream import BufferedStream
9
9
  from dissect.util.ts import from_unix
10
10
 
11
11
  from dissect.target.exceptions import UnsupportedPluginError
12
- from dissect.target.helpers.fsutil import TargetPath
12
+ from dissect.target.helpers.fsutil import TargetPath, open_decompress
13
13
  from dissect.target.helpers.record import TargetRecordDescriptor
14
- from dissect.target.plugin import OperatingSystem, Plugin, export
14
+ from dissect.target.plugin import Plugin, alias, export
15
15
  from dissect.target.target import Target
16
16
 
17
17
  UTMP_FIELDS = [
@@ -27,16 +27,12 @@ UTMP_FIELDS = [
27
27
 
28
28
  BtmpRecord = TargetRecordDescriptor(
29
29
  "linux/log/btmp",
30
- [
31
- *UTMP_FIELDS,
32
- ],
30
+ UTMP_FIELDS,
33
31
  )
34
32
 
35
33
  WtmpRecord = TargetRecordDescriptor(
36
34
  "linux/log/wtmp",
37
- [
38
- *UTMP_FIELDS,
39
- ],
35
+ UTMP_FIELDS,
40
36
  )
41
37
 
42
38
  utmp_def = """
@@ -104,24 +100,13 @@ UTMP_ENTRY = namedtuple(
104
100
  class UtmpFile:
105
101
  """utmp maintains a full accounting of the current status of the system"""
106
102
 
107
- def __init__(self, target: Target, path: TargetPath):
108
- self.fh = target.fs.path(path).open()
109
-
110
- if "gz" in path:
111
- self.compressed = True
112
- else:
113
- self.compressed = False
103
+ def __init__(self, path: TargetPath):
104
+ self.fh = open_decompress(path, "rb")
114
105
 
115
106
  def __iter__(self):
116
- if self.compressed:
117
- gzip_entry = BufferedStream(gzip.open(self.fh, mode="rb"))
118
- byte_stream = gzip_entry
119
- else:
120
- byte_stream = self.fh
121
-
122
107
  while True:
123
108
  try:
124
- entry = c_utmp.entry(byte_stream)
109
+ entry = c_utmp.entry(self.fh)
125
110
 
126
111
  r_type = ""
127
112
  if entry.ut_type in c_utmp.Type:
@@ -151,7 +136,7 @@ class UtmpFile:
151
136
  # ut_addr_v6 is parsed as IPv4 address. This could not lead to incorrect results.
152
137
  ut_addr = ipaddress.ip_address(struct.pack("<i", entry.ut_addr_v6[0]))
153
138
 
154
- utmp_entry = UTMP_ENTRY(
139
+ yield UTMP_ENTRY(
155
140
  ts=from_unix(entry.ut_tv.tv_sec),
156
141
  ut_type=r_type,
157
142
  ut_pid=entry.ut_pid,
@@ -162,7 +147,6 @@ class UtmpFile:
162
147
  ut_addr=ut_addr,
163
148
  )
164
149
 
165
- yield utmp_entry
166
150
  except EOFError:
167
151
  break
168
152
 
@@ -170,17 +154,28 @@ class UtmpFile:
170
154
  class UtmpPlugin(Plugin):
171
155
  """Unix utmp log plugin."""
172
156
 
173
- WTMP_GLOB = "/var/log/wtmp*"
174
- BTMP_GLOB = "/var/log/btmp*"
157
+ def __init__(self, target: Target):
158
+ super().__init__(target)
159
+ self.btmp_paths = list(self.target.fs.path("/").glob("var/log/btmp*"))
160
+ self.wtmp_paths = list(self.target.fs.path("/").glob("var/log/wtmp*"))
161
+ self.utmp_paths = list(self.target.fs.path("/").glob("var/run/utmp*"))
175
162
 
176
163
  def check_compatible(self) -> None:
177
- if not self.target.os == OperatingSystem.LINUX and not any(
178
- [
179
- list(self.target.fs.glob(self.BTMP_GLOB)),
180
- list(self.target.fs.glob(self.WTMP_GLOB)),
181
- ]
182
- ):
183
- raise UnsupportedPluginError("No WTMP or BTMP log files found")
164
+ if not any(self.btmp_paths + self.wtmp_paths + self.utmp_paths):
165
+ raise UnsupportedPluginError("No wtmp and/or btmp log files found")
166
+
167
+ def _build_record(self, record: TargetRecordDescriptor, entry: UTMP_ENTRY) -> Iterator[BtmpRecord | WtmpRecord]:
168
+ return record(
169
+ ts=entry.ts,
170
+ ut_type=entry.ut_type,
171
+ ut_pid=entry.ut_pid,
172
+ ut_user=entry.ut_user,
173
+ ut_line=entry.ut_line,
174
+ ut_id=entry.ut_id,
175
+ ut_host=entry.ut_host,
176
+ ut_addr=entry.ut_addr,
177
+ _target=self.target,
178
+ )
184
179
 
185
180
  @export(record=BtmpRecord)
186
181
  def btmp(self) -> Iterator[BtmpRecord]:
@@ -192,26 +187,18 @@ class UtmpPlugin(Plugin):
192
187
  - https://en.wikipedia.org/wiki/Utmp
193
188
  - https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
194
189
  """
195
- btmp_paths = self.target.fs.glob(self.BTMP_GLOB)
196
- for btmp_path in btmp_paths:
197
- btmp = UtmpFile(self.target, btmp_path)
198
-
199
- for entry in btmp:
200
- yield BtmpRecord(
201
- ts=entry.ts,
202
- ut_type=entry.ut_type,
203
- ut_pid=entry.ut_pid,
204
- ut_user=entry.ut_user,
205
- ut_line=entry.ut_line,
206
- ut_id=entry.ut_id,
207
- ut_host=entry.ut_host,
208
- ut_addr=entry.ut_addr,
209
- _target=self.target,
210
- )
190
+ for path in self.btmp_paths:
191
+ if not path.is_file():
192
+ self.target.log.warning("Unable to parse btmp file: %s is not a file", path)
193
+ continue
211
194
 
195
+ for entry in UtmpFile(path):
196
+ yield self._build_record(BtmpRecord, entry)
197
+
198
+ @alias("utmp")
212
199
  @export(record=WtmpRecord)
213
200
  def wtmp(self) -> Iterator[WtmpRecord]:
214
- """Return the content of the wtmp log files.
201
+ """Yield contents of wtmp log files.
215
202
 
216
203
  The wtmp file contains the historical data of the utmp file. The utmp file contains information about users
217
204
  logins at which terminals, logouts, system events and current status of the system, system boot time
@@ -220,19 +207,11 @@ class UtmpPlugin(Plugin):
220
207
  References:
221
208
  - https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
222
209
  """
223
- wtmp_paths = self.target.fs.glob(self.WTMP_GLOB)
224
- for wtmp_path in wtmp_paths:
225
- wtmp = UtmpFile(self.target, wtmp_path)
226
-
227
- for entry in wtmp:
228
- yield WtmpRecord(
229
- ts=entry.ts,
230
- ut_type=entry.ut_type,
231
- ut_pid=entry.ut_pid,
232
- ut_user=entry.ut_user,
233
- ut_line=entry.ut_line,
234
- ut_id=entry.ut_id,
235
- ut_host=entry.ut_host,
236
- ut_addr=entry.ut_addr,
237
- _target=self.target,
238
- )
210
+
211
+ for path in self.wtmp_paths + self.utmp_paths:
212
+ if not path.is_file():
213
+ self.target.log.warning("Unable to parse wtmp file: %s is not a file", path)
214
+ continue
215
+
216
+ for entry in UtmpFile(path):
217
+ yield self._build_record(WtmpRecord, entry)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.20.dev45
3
+ Version: 3.20.dev48
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,11 +268,11 @@ 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
275
- dissect/target/plugins/os/unix/log/utmp.py,sha256=n22dcjhclky3koF4MyRz1cBOmEeWwen6jstLsvfnMw8,7784
275
+ dissect/target/plugins/os/unix/log/utmp.py,sha256=k2A69s2qUT2JunJrH8GO6nQ0zMDotXMTaj8OzQ7ljj8,7336
276
276
  dissect/target/plugins/os/windows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
277
277
  dissect/target/plugins/os/windows/_os.py,sha256=-Bsp9696JqU7luh_AbqojzG9BxVdYIFl5Ma-LiFBQBo,12505
278
278
  dissect/target/plugins/os/windows/activitiescache.py,sha256=BbGD-vETHm1IRMoazVer_vqSJIoQxxhWcJ_xlBeOMds,6899
@@ -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.dev48.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
382
+ dissect.target-3.20.dev48.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
383
+ dissect.target-3.20.dev48.dist-info/METADATA,sha256=coXjiskalhbUYyedrn0Fe49ZmJvIbeAaMYawTB3Q9QQ,12897
384
+ dissect.target-3.20.dev48.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
385
+ dissect.target-3.20.dev48.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
386
+ dissect.target-3.20.dev48.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
387
+ dissect.target-3.20.dev48.dist-info/RECORD,,