dissect.target 3.20.dev45__py3-none-any.whl → 3.20.dev48__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/plugins/os/unix/log/auth.py +350 -39
- dissect/target/plugins/os/unix/log/utmp.py +47 -68
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/METADATA +1 -1
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/RECORD +9 -9
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/LICENSE +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/WHEEL +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/top_level.txt +0 -0
@@ -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
|
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.
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
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
|
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,
|
108
|
-
self.fh =
|
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(
|
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
|
-
|
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
|
-
|
174
|
-
|
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.
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
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
|
-
"""
|
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
|
-
|
224
|
-
for
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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.
|
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=
|
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=
|
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.
|
382
|
-
dissect.target-3.20.
|
383
|
-
dissect.target-3.20.
|
384
|
-
dissect.target-3.20.
|
385
|
-
dissect.target-3.20.
|
386
|
-
dissect.target-3.20.
|
387
|
-
dissect.target-3.20.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev48.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|