dissect.target 3.20.dev64__py3-none-any.whl → 3.20.2.dev11__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.
Files changed (34) hide show
  1. dissect/target/helpers/configutil.py +5 -5
  2. dissect/target/helpers/record.py +12 -6
  3. dissect/target/helpers/regutil.py +28 -11
  4. dissect/target/helpers/utils.py +20 -1
  5. dissect/target/loaders/itunes.py +5 -3
  6. dissect/target/plugins/apps/browser/iexplore.py +7 -3
  7. dissect/target/plugins/general/network.py +1 -1
  8. dissect/target/plugins/general/plugins.py +1 -1
  9. dissect/target/plugins/os/unix/_os.py +1 -1
  10. dissect/target/plugins/os/unix/bsd/osx/network.py +2 -1
  11. dissect/target/plugins/os/unix/esxi/_os.py +34 -32
  12. dissect/target/plugins/os/unix/etc/etc.py +12 -6
  13. dissect/target/plugins/os/unix/linux/fortios/_keys.py +7914 -1114
  14. dissect/target/plugins/os/unix/linux/fortios/_os.py +109 -22
  15. dissect/target/plugins/os/unix/linux/network.py +338 -0
  16. dissect/target/plugins/os/unix/linux/network_managers.py +1 -1
  17. dissect/target/plugins/os/unix/log/auth.py +6 -37
  18. dissect/target/plugins/os/unix/log/helpers.py +46 -0
  19. dissect/target/plugins/os/unix/log/messages.py +24 -15
  20. dissect/target/plugins/os/unix/trash.py +13 -2
  21. dissect/target/plugins/os/windows/activitiescache.py +32 -30
  22. dissect/target/plugins/os/windows/catroot.py +14 -5
  23. dissect/target/plugins/os/windows/lnk.py +13 -7
  24. dissect/target/plugins/os/windows/network.py +9 -5
  25. dissect/target/plugins/os/windows/notifications.py +40 -38
  26. dissect/target/plugins/os/windows/regf/cit.py +20 -7
  27. dissect/target/tools/diff.py +990 -0
  28. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/METADATA +73 -73
  29. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/RECORD +34 -31
  30. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/WHEEL +1 -1
  31. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/entry_points.txt +1 -0
  32. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/COPYRIGHT +0 -0
  33. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/LICENSE +0 -0
  34. {dissect.target-3.20.dev64.dist-info → dissect.target-3.20.2.dev11.dist-info}/top_level.txt +0 -0
@@ -11,12 +11,18 @@ from dissect.target.helpers.fsutil import open_decompress
11
11
  from dissect.target.helpers.record import TargetRecordDescriptor
12
12
  from dissect.target.helpers.utils import year_rollover_helper
13
13
  from dissect.target.plugin import Plugin, alias, export
14
+ from dissect.target.plugins.os.unix.log.helpers import (
15
+ RE_LINE,
16
+ RE_TS,
17
+ is_iso_fmt,
18
+ iso_readlines,
19
+ )
14
20
 
15
21
  MessagesRecord = TargetRecordDescriptor(
16
22
  "linux/log/messages",
17
23
  [
18
24
  ("datetime", "ts"),
19
- ("string", "daemon"),
25
+ ("string", "service"),
20
26
  ("varint", "pid"),
21
27
  ("string", "message"),
22
28
  ("path", "source"),
@@ -24,12 +30,8 @@ MessagesRecord = TargetRecordDescriptor(
24
30
  )
25
31
 
26
32
  DEFAULT_TS_LOG_FORMAT = "%b %d %H:%M:%S"
27
- RE_TS = re.compile(r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})")
28
- RE_DAEMON = re.compile(r"^[^:]+:\d+:\d+[^\[\]:]+\s([^\[:]+)[\[|:]{1}")
29
- RE_PID = re.compile(r"\w\[(\d+)\]")
30
- RE_MSG = re.compile(r"[^:]+:\d+:\d+[^:]+:\s(.*)$")
31
33
  RE_CLOUD_INIT_LINE = re.compile(
32
- r"^(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?P<daemon>.*)\[(?P<log_level>\w+)\]\: (?P<message>.*)$"
34
+ r"^(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?P<service>.*)\[(?P<log_level>\w+)\]\: (?P<message>.*)$"
33
35
  )
34
36
 
35
37
 
@@ -56,7 +58,7 @@ class MessagesPlugin(Plugin):
56
58
  def messages(self) -> Iterator[MessagesRecord]:
57
59
  """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
58
60
 
59
- Due to year rollover detection, the contents of the files are returned in reverse.
61
+ Due to year rollover detection, the log contents could be returned in reversed or mixed chronological order.
60
62
 
61
63
  The messages log file holds information about a variety of events such as the system error messages, system
62
64
  startups and shutdowns, change in the network configuration, etc. Aims to store valuable, non-debug and
@@ -75,16 +77,23 @@ class MessagesPlugin(Plugin):
75
77
  yield from self._parse_cloud_init_log(log_file, tzinfo)
76
78
  continue
77
79
 
78
- for ts, line in year_rollover_helper(log_file, RE_TS, DEFAULT_TS_LOG_FORMAT, tzinfo):
79
- daemon = dict(enumerate(RE_DAEMON.findall(line))).get(0)
80
- pid = dict(enumerate(RE_PID.findall(line))).get(0)
81
- message = dict(enumerate(RE_MSG.findall(line))).get(0, line)
80
+ if is_iso_fmt(log_file):
81
+ iterable = iso_readlines(log_file)
82
+
83
+ else:
84
+ iterable = year_rollover_helper(log_file, RE_TS, DEFAULT_TS_LOG_FORMAT, tzinfo)
85
+
86
+ for ts, line in iterable:
87
+ match = RE_LINE.search(line)
88
+
89
+ if not match:
90
+ self.target.log.warning("Unable to parse message line in %s", log_file)
91
+ self.target.log.debug("Line %s", line)
92
+ continue
82
93
 
83
94
  yield MessagesRecord(
84
95
  ts=ts,
85
- daemon=daemon,
86
- pid=pid,
87
- message=message,
96
+ **match.groupdict(),
88
97
  source=log_file,
89
98
  _target=self.target,
90
99
  )
@@ -134,7 +143,7 @@ class MessagesPlugin(Plugin):
134
143
 
135
144
  yield MessagesRecord(
136
145
  ts=ts,
137
- daemon=values["daemon"],
146
+ service=values["service"],
138
147
  pid=None,
139
148
  message=values["message"],
140
149
  source=log_file,
@@ -31,15 +31,25 @@ class GnomeTrashPlugin(Plugin):
31
31
 
32
32
  def __init__(self, target: Target):
33
33
  super().__init__(target)
34
- self.trashes = list(self._garbage_collector())
34
+ self.trashes = set(self._garbage_collector())
35
35
 
36
36
  def _garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]:
37
37
  """it aint much, but its honest work"""
38
+
39
+ # home trash folders
38
40
  for user_details in self.target.user_details.all_with_home():
39
41
  for trash_path in self.PATHS:
40
42
  if (path := user_details.home_path.joinpath(trash_path)).exists():
41
43
  yield user_details, path
42
44
 
45
+ # mounted devices trash folders
46
+ for mount_path in list(self.target.fs.mounts) + ["/mnt", "/media"]:
47
+ if mount_path == "/":
48
+ continue
49
+
50
+ for mount_trash in self.target.fs.path(mount_path).rglob(".Trash-*"):
51
+ yield UserDetails(None, None), mount_trash
52
+
43
53
  def check_compatible(self) -> None:
44
54
  if not self.trashes:
45
55
  raise UnsupportedPluginError("No Trash folder(s) found")
@@ -52,7 +62,8 @@ class GnomeTrashPlugin(Plugin):
52
62
  Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``.
53
63
  Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop.
54
64
 
55
- Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``.
65
+ Also parses media trash locations such as ``/media/$USER/$Label/.Trash-*``, ``/mnt/$Label/.Trash-*`` and other
66
+ locations as defined in ``/etc/fstab``.
56
67
 
57
68
  Resources:
58
69
  - https://specifications.freedesktop.org/trash-spec/latest/
@@ -116,36 +116,38 @@ class ActivitiesCachePlugin(Plugin):
116
116
  for user, cache_file in self.cachefiles:
117
117
  fh = cache_file.open()
118
118
  db = sqlite3.SQLite3(fh)
119
- for r in db.table("Activity").rows():
120
- yield ActivitiesCacheRecord(
121
- start_time=mkts(r["[StartTime]"]),
122
- end_time=mkts(r["[EndTime]"]),
123
- last_modified_time=mkts(r["[LastModifiedTime]"]),
124
- last_modified_on_client=mkts(r["[LastModifiedOnClient]"]),
125
- original_last_modified_on_client=mkts(r["[OriginalLastModifiedOnClient]"]),
126
- expiration_time=mkts(r["[ExpirationTime]"]),
127
- app_id=r["[AppId]"],
128
- enterprise_id=r["[EnterpriseId]"],
129
- app_activity_id=r["[AppActivityId]"],
130
- group_app_activity_id=r["[GroupAppActivityId]"],
131
- group=r["[Group]"],
132
- activity_type=r["[ActivityType]"],
133
- activity_status=r["[ActivityStatus]"],
134
- priority=r["[Priority]"],
135
- match_id=r["[MatchId]"],
136
- etag=r["[ETag]"],
137
- tag=r["[Tag]"],
138
- is_local_only=r["[IsLocalOnly]"],
139
- created_in_cloud=r["[CreatedInCloud]"],
140
- platform_device_id=r["[PlatformDeviceId]"],
141
- package_id_hash=r["[PackageIdHash]"],
142
- id=r["[Id]"],
143
- payload=r["[Payload]"],
144
- original_payload=r["[OriginalPayload]"],
145
- clipboard_payload=r["[ClipboardPayload]"],
146
- _target=self.target,
147
- _user=user,
148
- )
119
+
120
+ if table := db.table("Activity"):
121
+ for r in table.rows():
122
+ yield ActivitiesCacheRecord(
123
+ start_time=mkts(r["[StartTime]"]),
124
+ end_time=mkts(r["[EndTime]"]),
125
+ last_modified_time=mkts(r["[LastModifiedTime]"]),
126
+ last_modified_on_client=mkts(r["[LastModifiedOnClient]"]),
127
+ original_last_modified_on_client=mkts(r["[OriginalLastModifiedOnClient]"]),
128
+ expiration_time=mkts(r["[ExpirationTime]"]),
129
+ app_id=r["[AppId]"],
130
+ enterprise_id=r["[EnterpriseId]"],
131
+ app_activity_id=r["[AppActivityId]"],
132
+ group_app_activity_id=r["[GroupAppActivityId]"],
133
+ group=r["[Group]"],
134
+ activity_type=r["[ActivityType]"],
135
+ activity_status=r["[ActivityStatus]"],
136
+ priority=r["[Priority]"],
137
+ match_id=r["[MatchId]"],
138
+ etag=r["[ETag]"],
139
+ tag=r["[Tag]"],
140
+ is_local_only=r["[IsLocalOnly]"],
141
+ created_in_cloud=r["[CreatedInCloud]"],
142
+ platform_device_id=r["[PlatformDeviceId]"],
143
+ package_id_hash=r["[PackageIdHash]"],
144
+ id=r["[Id]"],
145
+ payload=r["[Payload]"],
146
+ original_payload=r["[OriginalPayload]"],
147
+ clipboard_payload=r["[ClipboardPayload]"],
148
+ _target=self.target,
149
+ _user=user,
150
+ )
149
151
 
150
152
 
151
153
  def mkts(ts: int) -> datetime | None:
@@ -217,15 +217,24 @@ class CatrootPlugin(Plugin):
217
217
  with ese_file.open("rb") as fh:
218
218
  ese_db = EseDB(fh)
219
219
 
220
- tables = [table.name for table in ese_db.tables()]
221
220
  for hash_type, table_name in [("sha256", "HashCatNameTableSHA256"), ("sha1", "HashCatNameTableSHA1")]:
222
- if table_name not in tables:
221
+ try:
222
+ table = ese_db.table(table_name)
223
+ except KeyError as e:
224
+ self.target.log.warning("EseDB %s has no table %s", ese_file, table_name)
225
+ self.target.log.debug("", exc_info=e)
223
226
  continue
224
227
 
225
- for record in ese_db.table(table_name).records():
228
+ for record in table.records():
226
229
  file_digest = digest()
227
- setattr(file_digest, hash_type, record.get("HashCatNameTable_HashCol").hex())
228
- catroot_names = record.get("HashCatNameTable_CatNameCol").decode().rstrip("|").split("|")
230
+
231
+ try:
232
+ setattr(file_digest, hash_type, record.get("HashCatNameTable_HashCol").hex())
233
+ catroot_names = record.get("HashCatNameTable_CatNameCol").decode().rstrip("|").split("|")
234
+ except Exception as e:
235
+ self.target.log.warning("Unable to parse catroot names for %s in %s", record, ese_file)
236
+ self.target.log.debug("", exc_info=e)
237
+ continue
229
238
 
230
239
  for catroot_name in catroot_names:
231
240
  yield CatrootRecord(
@@ -1,4 +1,6 @@
1
- from typing import Iterator, Optional
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterator
2
4
 
3
5
  from dissect.shellitem.lnk import Lnk
4
6
  from dissect.util import ts
@@ -34,7 +36,7 @@ LnkRecord = TargetRecordDescriptor(
34
36
  )
35
37
 
36
38
 
37
- def parse_lnk_file(target: Target, lnk_file: Lnk, lnk_path: TargetPath) -> Iterator[LnkRecord]:
39
+ def parse_lnk_file(target: Target, lnk_file: Lnk, lnk_path: TargetPath) -> LnkRecord:
38
40
  # we need to get the active codepage from the system to properly decode some values
39
41
  codepage = target.codepage or "ascii"
40
42
 
@@ -132,7 +134,7 @@ class LnkPlugin(Plugin):
132
134
 
133
135
  @arg("--path", "-p", dest="path", default=None, help="Path to directory or .lnk file in target")
134
136
  @export(record=LnkRecord)
135
- def lnk(self, path: Optional[str] = None) -> Iterator[LnkRecord]:
137
+ def lnk(self, path: str | None = None) -> Iterator[LnkRecord]:
136
138
  """Parse all .lnk files in /ProgramData, /Users, and /Windows or from a specified path in record format.
137
139
 
138
140
  Yields a LnkRecord record with the following fields:
@@ -160,10 +162,14 @@ class LnkPlugin(Plugin):
160
162
  """
161
163
 
162
164
  for entry in self.lnk_entries(path):
163
- lnk_file = Lnk(entry.open())
164
- yield parse_lnk_file(self.target, lnk_file, entry)
165
-
166
- def lnk_entries(self, path: Optional[str] = None) -> Iterator[TargetPath]:
165
+ try:
166
+ lnk_file = Lnk(entry.open())
167
+ yield parse_lnk_file(self.target, lnk_file, entry)
168
+ except Exception as e:
169
+ self.target.log.warning("Failed to parse link file %s", lnk_file)
170
+ self.target.log.debug("", exc_info=e)
171
+
172
+ def lnk_entries(self, path: str | None = None) -> Iterator[TargetPath]:
167
173
  if path:
168
174
  target_path = self.target.fs.path(path)
169
175
  if not target_path.exists():
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from enum import IntEnum
4
5
  from functools import lru_cache
5
6
  from typing import Iterator
@@ -224,11 +225,13 @@ def _try_value(subkey: RegistryKey, value: str) -> str | list | None:
224
225
  return None
225
226
 
226
227
 
227
- def _get_config_value(key: RegistryKey, name: str) -> set:
228
+ def _get_config_value(key: RegistryKey, name: str, sep: str | None = None) -> set:
228
229
  value = _try_value(key, name)
229
230
  if not value or value in ("", "0.0.0.0", None, [], ["0.0.0.0"]):
230
231
  return set()
231
-
232
+ if sep and isinstance(value, str):
233
+ re_sep = "|".join(map(re.escape, sep))
234
+ value = re.split(re_sep, value)
232
235
  if isinstance(value, list):
233
236
  return set(value)
234
237
 
@@ -281,7 +284,8 @@ class WindowsNetworkPlugin(NetworkPlugin):
281
284
  pass
282
285
 
283
286
  # Extract the rest of the device information
284
- device_info["mac"] = _try_value(subkey, "NetworkAddress")
287
+ if mac_address := _try_value(subkey, "NetworkAddress"):
288
+ device_info["mac"] = [mac_address]
285
289
  device_info["vlan"] = _try_value(subkey, "VlanID")
286
290
 
287
291
  if timestamp := _try_value(subkey, "NetworkInterfaceInstallTimestamp"):
@@ -354,11 +358,11 @@ class WindowsNetworkPlugin(NetworkPlugin):
354
358
  dhcp_config["ip"].update(_get_config_value(key, "DhcpIPAddress"))
355
359
  dhcp_config["subnetmask"].update(_get_config_value(key, "DhcpSubnetMask"))
356
360
  dhcp_config["search_domain"].update(_get_config_value(key, "DhcpDomain"))
357
- dhcp_config["dns"].update(_get_config_value(key, "DhcpNameServer"))
361
+ dhcp_config["dns"].update(_get_config_value(key, "DhcpNameServer", " ,"))
358
362
 
359
363
  # Extract static configuration from the registry
360
364
  static_config["gateway"].update(_get_config_value(key, "DefaultGateway"))
361
- static_config["dns"].update(_get_config_value(key, "NameServer"))
365
+ static_config["dns"].update(_get_config_value(key, "NameServer", " ,"))
362
366
  static_config["search_domain"].update(_get_config_value(key, "Domain"))
363
367
  static_config["ip"].update(_get_config_value(key, "IPAddress"))
364
368
  static_config["subnetmask"].update(_get_config_value(key, "SubnetMask"))
@@ -442,43 +442,45 @@ class NotificationsPlugin(Plugin):
442
442
  """
443
443
  for user, wpndatabase in self.wpndb_files:
444
444
  db = sqlite3.SQLite3(wpndatabase.open())
445
-
446
445
  handlers = {}
447
- for row in db.table("NotificationHandler").rows():
448
- handlers[row["[RecordId]"]] = WpnDatabaseNotificationHandlerRecord(
449
- created_time=datetime.datetime.strptime(row["[CreatedTime]"], "%Y-%m-%d %H:%M:%S"),
450
- modified_time=datetime.datetime.strptime(row["[ModifiedTime]"], "%Y-%m-%d %H:%M:%S"),
451
- id=row["[RecordId]"],
452
- primary_id=row["[PrimaryId]"],
453
- wns_id=row["[WNSId]"],
454
- handler_type=row["[HandlerType]"],
455
- wnf_event_name=row["[WNFEventName]"],
456
- system_data_property_set=row["[SystemDataPropertySet]"],
457
- _target=self.target,
458
- _user=user,
459
- )
460
-
461
- for row in db.table("Notification").rows():
462
- record = WpnDatabaseNotificationRecord(
463
- arrival_time=wintimestamp(row["[ArrivalTime]"]),
464
- expiry_time=wintimestamp(row["[ExpiryTime]"]),
465
- order=row["[Order]"],
466
- id=row["[Id]"],
467
- handler_id=row["[HandlerId]"],
468
- activity_id=UUID(bytes=row["[ActivityId]"]),
469
- type=row["[Type]"],
470
- payload=row["[Payload]"],
471
- payload_type=row["[PayloadType]"],
472
- tag=row["[Tag]"],
473
- group=row["[Group]"],
474
- boot_id=row["[BootId]"],
475
- expires_on_reboot=row["[ExpiresOnReboot]"] != "FALSE",
476
- _target=self.target,
477
- _user=user,
478
- )
479
- handler = handlers.get(row["[HandlerId]"])
480
446
 
481
- if handler:
482
- yield GroupedRecord("windows/notification/wpndatabase/grouped", [record, handler])
483
- else:
484
- yield record
447
+ if table := db.table("NotificationHandler"):
448
+ for row in table.rows():
449
+ handlers[row["[RecordId]"]] = WpnDatabaseNotificationHandlerRecord(
450
+ created_time=datetime.datetime.strptime(row["[CreatedTime]"], "%Y-%m-%d %H:%M:%S"),
451
+ modified_time=datetime.datetime.strptime(row["[ModifiedTime]"], "%Y-%m-%d %H:%M:%S"),
452
+ id=row["[RecordId]"],
453
+ primary_id=row["[PrimaryId]"],
454
+ wns_id=row["[WNSId]"],
455
+ handler_type=row["[HandlerType]"],
456
+ wnf_event_name=row["[WNFEventName]"],
457
+ system_data_property_set=row["[SystemDataPropertySet]"],
458
+ _target=self.target,
459
+ _user=user,
460
+ )
461
+
462
+ if table := db.table("Notification"):
463
+ for row in table.rows():
464
+ record = WpnDatabaseNotificationRecord(
465
+ arrival_time=wintimestamp(row["[ArrivalTime]"]),
466
+ expiry_time=wintimestamp(row["[ExpiryTime]"]),
467
+ order=row["[Order]"],
468
+ id=row["[Id]"],
469
+ handler_id=row["[HandlerId]"],
470
+ activity_id=UUID(bytes=row["[ActivityId]"]),
471
+ type=row["[Type]"],
472
+ payload=row["[Payload]"],
473
+ payload_type=row["[PayloadType]"],
474
+ tag=row["[Tag]"],
475
+ group=row["[Group]"],
476
+ boot_id=row["[BootId]"],
477
+ expires_on_reboot=row["[ExpiresOnReboot]"] != "FALSE",
478
+ _target=self.target,
479
+ _user=user,
480
+ )
481
+ handler = handlers.get(row["[HandlerId]"])
482
+
483
+ if handler:
484
+ yield GroupedRecord("windows/notification/wpndatabase/grouped", [record, handler])
485
+ else:
486
+ yield record
@@ -632,8 +632,8 @@ def local_wintimestamp(target, ts):
632
632
  class CITPlugin(Plugin):
633
633
  """Plugin that parses CIT data from the registry.
634
634
 
635
- Reference:
636
- - https://dfir.ru/2018/12/02/the-cit-database-and-the-syscache-hive/
635
+ References:
636
+ - https://dfir.ru/2018/12/02/the-cit-database-and-the-syscache-hive/
637
637
  """
638
638
 
639
639
  __namespace__ = "cit"
@@ -641,7 +641,7 @@ class CITPlugin(Plugin):
641
641
  KEY = "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\CIT"
642
642
 
643
643
  def check_compatible(self) -> None:
644
- if not len(list(self.target.registry.keys(self.KEY))) > 0:
644
+ if not list(self.target.registry.keys(self.KEY)):
645
645
  raise UnsupportedPluginError("No CIT registry key found")
646
646
 
647
647
  @export(record=get_args(CITRecords))
@@ -770,8 +770,9 @@ class CITPlugin(Plugin):
770
770
  yield from _yield_bitmap_records(
771
771
  self.target, cit, entry.use_data.bitmaps.foreground, CITProgramBitmapForegroundRecord
772
772
  )
773
- except Exception:
774
- self.target.log.exception("Failed to parse CIT value: %s", value.name)
773
+ except Exception as e:
774
+ self.target.log.warning("Failed to parse CIT value: %s", value.name)
775
+ self.target.log.debug("", exc_info=e)
775
776
 
776
777
  @export(record=CITPostUpdateUseInfoRecord)
777
778
  def puu(self) -> Iterator[CITPostUpdateUseInfoRecord]:
@@ -788,10 +789,16 @@ class CITPlugin(Plugin):
788
789
  for reg_key in keys:
789
790
  for key in self.target.registry.keys(reg_key):
790
791
  try:
791
- puu = c_cit.CIT_POST_UPDATE_USE_INFO(key.value("PUUActive").value)
792
+ key_value = key.value("PUUActive").value
793
+ puu = c_cit.CIT_POST_UPDATE_USE_INFO(key_value)
792
794
  except RegistryValueNotFoundError:
793
795
  continue
794
796
 
797
+ except EOFError as e:
798
+ self.target.log.warning("Exception reading CIT structure in key %s", key.path)
799
+ self.target.log.debug("Unable to parse value %s", key_value, exc_info=e)
800
+ continue
801
+
795
802
  yield CITPostUpdateUseInfoRecord(
796
803
  log_time_start=wintimestamp(puu.LogTimeStart),
797
804
  update_key=puu.UpdateKey,
@@ -852,10 +859,16 @@ class CITPlugin(Plugin):
852
859
  for reg_key in keys:
853
860
  for key in self.target.registry.keys(reg_key):
854
861
  try:
855
- dp = c_cit.CIT_DP_DATA(key.value("DP").value)
862
+ key_value = key.value("DP").value
863
+ dp = c_cit.CIT_DP_DATA(key_value)
856
864
  except RegistryValueNotFoundError:
857
865
  continue
858
866
 
867
+ except EOFError as e:
868
+ self.target.log.warning("Exception reading CIT structure in key %s", key.path)
869
+ self.target.log.debug("Unable to parse value %s", key_value, exc_info=e)
870
+ continue
871
+
859
872
  user = self.target.registry.get_user(key)
860
873
  log_time_start = wintimestamp(dp.LogTimeStart)
861
874