dissect.target 3.17.dev26__py3-none-any.whl → 3.17.dev27__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@ import re
5
5
  from collections import defaultdict
6
6
  from configparser import ConfigParser, MissingSectionHeaderError
7
7
  from io import StringIO
8
+ from itertools import chain
8
9
  from re import compile, sub
9
10
  from typing import Any, Callable, Iterable, Match, Optional
10
11
 
@@ -299,7 +300,8 @@ class Parser:
299
300
  return
300
301
 
301
302
  if section:
302
- config = config.get(section, {})
303
+ # account for values of sections which are None
304
+ config = config.get(section, {}) or {}
303
305
 
304
306
  for key, value in config.items():
305
307
  if key == option:
@@ -508,7 +510,7 @@ class LinuxNetworkManager:
508
510
 
509
511
 
510
512
  def parse_unix_dhcp_log_messages(target) -> list[str]:
511
- """Parse local syslog and cloud init log files for DHCP lease IPs.
513
+ """Parse local syslog, journal and cloud init-log files for DHCP lease IPs.
512
514
 
513
515
  Args:
514
516
  target: Target to discover and obtain network information from.
@@ -516,53 +518,68 @@ def parse_unix_dhcp_log_messages(target) -> list[str]:
516
518
  Returns:
517
519
  List of DHCP ip addresses.
518
520
  """
519
- ips = []
520
-
521
- # Search through parsed syslogs for DHCP leases.
522
- try:
523
- messages = target.messages()
524
- for record in messages:
525
- line = record.message
526
-
527
- # Ubuntu DHCP
528
- if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
529
- ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
530
- if ip not in ips:
531
- ips.append(ip)
532
-
533
- # Ubuntu DHCP NetworkManager
534
- elif "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
535
- ip = line.split("=> '")[1].replace("'", "").strip()
536
- if ip not in ips:
537
- ips.append(ip)
538
-
539
- # Debian and CentOS dhclient
540
- elif record.daemon == "dhclient" and "bound to" in line:
541
- ip = line.split("bound to")[1].split(" ")[1].strip()
542
- if ip not in ips:
543
- ips.append(ip)
544
-
545
- # CentOS DHCP and general NetworkManager
546
- elif " address " in line and ("dhcp4" in line or "dhcp6" in line):
547
- ip = line.split(" address ")[1].strip()
548
- if ip not in ips:
549
- ips.append(ip)
550
-
551
- except PluginError:
552
- target.log.debug("Can not search for DHCP leases in syslog files as they does not exist.")
553
-
554
- # A unix system might be provisioned using Ubuntu's cloud-init (https://cloud-init.io/).
555
- if (path := target.fs.path("/var/log/cloud-init.log")).exists():
556
- for line in path.open("rt"):
557
- # We are interested in the following log line:
558
- # YYYY-MM-DD HH:MM:SS,000 - dhcp.py[DEBUG]: Received dhcp lease on IFACE for IP/MASK
559
- if "Received dhcp lease on" in line:
560
- interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
561
- if ip not in ips:
562
- ips.append(ip)
563
-
564
- if not path and not messages:
565
- target.log.warning("Can not search for DHCP leases in syslog or cloud-init.log files as they does not exist.")
521
+ ips = set()
522
+ messages = set()
523
+
524
+ for log_func in ["messages", "journal"]:
525
+ try:
526
+ messages = chain(messages, getattr(target, log_func)())
527
+ except PluginError:
528
+ target.log.debug(f"Could not search for DHCP leases in {log_func} files.")
529
+
530
+ if not messages:
531
+ target.log.warning(f"Could not search for DHCP leases using {log_func}: No log entries found.")
532
+
533
+ for record in messages:
534
+ line = record.message
535
+
536
+ # Ubuntu cloud-init
537
+ if "Received dhcp lease on" in line:
538
+ interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
539
+ ips.add(ip)
540
+ continue
541
+
542
+ # Ubuntu DHCP
543
+ if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
544
+ ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
545
+ ips.add(ip)
546
+ continue
547
+
548
+ # Ubuntu DHCP NetworkManager
549
+ if "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
550
+ ip = line.split("=> '")[1].replace("'", "").strip()
551
+ ips.add(ip)
552
+ continue
553
+
554
+ # Debian and CentOS dhclient
555
+ if hasattr(record, "daemon") and record.daemon == "dhclient" and "bound to" in line:
556
+ ip = line.split("bound to")[1].split(" ")[1].strip()
557
+ ips.add(ip)
558
+ continue
559
+
560
+ # CentOS DHCP and general NetworkManager
561
+ if " address " in line and ("dhcp4" in line or "dhcp6" in line):
562
+ ip = line.split(" address ")[1].strip()
563
+ ips.add(ip)
564
+ continue
565
+
566
+ # Ubuntu/Debian DHCP networkd (Journal)
567
+ if (
568
+ hasattr(record, "code_func")
569
+ and record.code_func == "dhcp_lease_acquired"
570
+ and " address " in line
571
+ and " via " in line
572
+ ):
573
+ interface, ip, netmask, gateway = re.search(
574
+ r"^(\S+): DHCPv[4|6] address (\S+)\/(\S+) via (\S+)", line
575
+ ).groups()
576
+ ips.add(ip)
577
+ continue
578
+
579
+ # Journals and syslogs can be large and slow to iterate,
580
+ # so we stop if we have some results and have reached the journal plugin.
581
+ if len(ips) >= 2 and record._desc.name == "linux/log/journal":
582
+ break
566
583
 
567
584
  return ips
568
585
 
@@ -1,7 +1,8 @@
1
1
  import re
2
- from itertools import chain
2
+ from pathlib import Path
3
3
  from typing import Iterator
4
4
 
5
+ from dissect.target import Target
5
6
  from dissect.target.exceptions import UnsupportedPluginError
6
7
  from dissect.target.helpers.record import TargetRecordDescriptor
7
8
  from dissect.target.helpers.utils import year_rollover_helper
@@ -23,17 +24,28 @@ RE_TS = re.compile(r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})")
23
24
  RE_DAEMON = re.compile(r"^[^:]+:\d+:\d+[^\[\]:]+\s([^\[:]+)[\[|:]{1}")
24
25
  RE_PID = re.compile(r"\w\[(\d+)\]")
25
26
  RE_MSG = re.compile(r"[^:]+:\d+:\d+[^:]+:\s(.*)$")
27
+ RE_CLOUD_INIT_LINE = re.compile(r"(?P<ts>.*) - (?P<daemon>.*)\[(?P<log_level>\w+)\]\: (?P<message>.*)$")
26
28
 
27
29
 
28
30
  class MessagesPlugin(Plugin):
31
+ def __init__(self, target: Target):
32
+ super().__init__(target)
33
+ self.log_files = set(self._find_log_files())
34
+
35
+ def _find_log_files(self) -> Iterator[Path]:
36
+ log_dirs = ["/var/log/", "/var/log/installer/"]
37
+ file_globs = ["syslog*", "messages*", "cloud-init.log*"]
38
+ for log_dir in log_dirs:
39
+ for glob in file_globs:
40
+ yield from self.target.fs.path(log_dir).glob(glob)
41
+
29
42
  def check_compatible(self) -> None:
30
- var_log = self.target.fs.path("/var/log")
31
- if not any(var_log.glob("syslog*")) and not any(var_log.glob("messages*")):
32
- raise UnsupportedPluginError("No message files found")
43
+ if not self.log_files:
44
+ raise UnsupportedPluginError("No log files found")
33
45
 
34
46
  @export(record=MessagesRecord)
35
47
  def syslog(self) -> Iterator[MessagesRecord]:
36
- """Return contents of /var/log/messages* and /var/log/syslog*.
48
+ """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
37
49
 
38
50
  See ``messages`` for more information.
39
51
  """
@@ -41,7 +53,7 @@ class MessagesPlugin(Plugin):
41
53
 
42
54
  @export(record=MessagesRecord)
43
55
  def messages(self) -> Iterator[MessagesRecord]:
44
- """Return contents of /var/log/messages* and /var/log/syslog*.
56
+ """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
45
57
 
46
58
  Note: due to year rollover detection, the contents of the files are returned in reverse.
47
59
 
@@ -52,12 +64,16 @@ class MessagesPlugin(Plugin):
52
64
  References:
53
65
  - https://geek-university.com/linux/var-log-messages-file/
54
66
  - https://www.geeksforgeeks.org/file-timestamps-mtime-ctime-and-atime-in-linux/
67
+ - https://cloudinit.readthedocs.io/en/latest/development/logging.html#logging-command-output
55
68
  """
56
69
 
57
70
  tzinfo = self.target.datetime.tzinfo
58
71
 
59
- var_log = self.target.fs.path("/var/log")
60
- for log_file in chain(var_log.glob("syslog*"), var_log.glob("messages*")):
72
+ for log_file in self.log_files:
73
+ if "cloud-init" in log_file.name:
74
+ yield from self._parse_cloud_init_log(log_file)
75
+ continue
76
+
61
77
  for ts, line in year_rollover_helper(log_file, RE_TS, DEFAULT_TS_LOG_FORMAT, tzinfo):
62
78
  daemon = dict(enumerate(RE_DAEMON.findall(line))).get(0)
63
79
  pid = dict(enumerate(RE_PID.findall(line))).get(0)
@@ -71,3 +87,32 @@ class MessagesPlugin(Plugin):
71
87
  source=log_file,
72
88
  _target=self.target,
73
89
  )
90
+
91
+ def _parse_cloud_init_log(self, log_file: Path) -> Iterator[MessagesRecord]:
92
+ """Parse a cloud-init.log file.
93
+
94
+ Lines are structured in the following format:
95
+ ``YYYY-MM-DD HH:MM:SS,000 - dhcp.py[DEBUG]: Received dhcp lease on IFACE for IP/MASK``
96
+
97
+ NOTE: ``cloud-init-output.log`` files are not supported as they do not contain structured logs.
98
+
99
+ Args:
100
+ ``log_file``: path to cloud-init.log file.
101
+
102
+ Returns: ``MessagesRecord``
103
+ """
104
+ for line in log_file.open("rt").readlines():
105
+ if line := line.strip():
106
+ if match := RE_CLOUD_INIT_LINE.match(line):
107
+ match = match.groupdict()
108
+ yield MessagesRecord(
109
+ ts=match["ts"].split(",")[0],
110
+ daemon=match["daemon"],
111
+ pid=None,
112
+ message=match["message"],
113
+ source=log_file,
114
+ _target=self.target,
115
+ )
116
+ else:
117
+ self.target.log.warning("Could not match cloud-init log line")
118
+ self.target.log.debug("No match for line '%s'", line)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev26
3
+ Version: 3.17.dev27
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
@@ -57,7 +57,7 @@ dissect/target/helpers/loaderutil.py,sha256=kiyMWra_gVxfNSGwLlgxLcuuqAYuCMDc5NiC
57
57
  dissect/target/helpers/localeutil.py,sha256=Y4Fh4jDSGfm5356xSLMriUCN8SZP_FAHg_iodkAxNq4,1504
58
58
  dissect/target/helpers/mount.py,sha256=JxhUYyEbDnHfzPpfuWy4nV9OwCJPoDSGdHHNiyvd_l0,3949
59
59
  dissect/target/helpers/mui.py,sha256=i-7XoHbu4WO2fYapK9yGAMW04rFlgRispknc1KQIS5Q,22258
60
- dissect/target/helpers/network_managers.py,sha256=OgrYhbBqM6K5OfUnCdTLG0RBrR-DcpR1CPezbNddK7k,24667
60
+ dissect/target/helpers/network_managers.py,sha256=uRh_P8ICbKke2N7eFJ6AS2-I5DmIRiaQUlxR7oqxPaU,24975
61
61
  dissect/target/helpers/polypath.py,sha256=h8p7m_OCNiQljGwoZh5Aflr9H2ot6CZr6WKq1OSw58o,2175
62
62
  dissect/target/helpers/protobuf.py,sha256=NwKrZD4q9v7J8GnZX9gbzMUMV5pR78eAV17jgWOz_EY,1730
63
63
  dissect/target/helpers/record.py,sha256=lWl7k2Mp9Axllm0tXzPGJx2zj2zONsyY_p5g424T0Lc,4826
@@ -247,7 +247,7 @@ dissect/target/plugins/os/unix/log/audit.py,sha256=OjorWTmCFvCI5RJq6m6WNW0Lhb-po
247
247
  dissect/target/plugins/os/unix/log/auth.py,sha256=l7gCuRdvv9gL0U1N0yrR9hVsMnr4t_k4t-n-f6PrOxg,2388
248
248
  dissect/target/plugins/os/unix/log/journal.py,sha256=eiNNVLmKWFj4dTQX8PNRNgKpVwzQWEHEsKyYfGUAPXQ,17376
249
249
  dissect/target/plugins/os/unix/log/lastlog.py,sha256=eL_dbB1sPoy0tyavIjT457ZLVfXcCr17GiwDrMEEh8A,2458
250
- dissect/target/plugins/os/unix/log/messages.py,sha256=W3CeI0tchdRql9SKLFDxk9AKwUvqIrnpCujcERvDt90,2846
250
+ dissect/target/plugins/os/unix/log/messages.py,sha256=CXA-SkMPLaCgnTQg9nzII-7tO8Il_ENQmuYvDxo33rI,4698
251
251
  dissect/target/plugins/os/unix/log/utmp.py,sha256=21tvzG977LqzRShV6uAoU-83WDcLUrI_Tv__2ZVi9rw,7756
252
252
  dissect/target/plugins/os/windows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
253
253
  dissect/target/plugins/os/windows/_os.py,sha256=EA9B9Rgb1C9NMvlX3gXhTRFXYaI6zrrKRg0OYq4v1ts,12589
@@ -336,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
336
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
337
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
338
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
339
- dissect.target-3.17.dev26.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
- dissect.target-3.17.dev26.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
- dissect.target-3.17.dev26.dist-info/METADATA,sha256=fyuJSNpaOXUDx5rJFQYpsaxFKwa7VqFttG1XIvZxXco,11300
342
- dissect.target-3.17.dev26.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
- dissect.target-3.17.dev26.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
- dissect.target-3.17.dev26.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
- dissect.target-3.17.dev26.dist-info/RECORD,,
339
+ dissect.target-3.17.dev27.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev27.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev27.dist-info/METADATA,sha256=3-kTMZehcHT31jjm50J9_Msj1Pw6LqWUMsiMaSaLiBY,11300
342
+ dissect.target-3.17.dev27.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev27.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev27.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev27.dist-info/RECORD,,