dissect.target 3.20.dev64__py3-none-any.whl → 3.20.2.dev11__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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