dissect.target 3.18.dev15__py3-none-any.whl → 3.19__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. dissect/target/filesystem.py +44 -25
  2. dissect/target/filesystems/config.py +32 -21
  3. dissect/target/filesystems/extfs.py +4 -0
  4. dissect/target/filesystems/itunes.py +1 -1
  5. dissect/target/filesystems/tar.py +1 -1
  6. dissect/target/filesystems/zip.py +81 -46
  7. dissect/target/helpers/config.py +22 -7
  8. dissect/target/helpers/configutil.py +69 -5
  9. dissect/target/helpers/cyber.py +4 -2
  10. dissect/target/helpers/fsutil.py +32 -4
  11. dissect/target/helpers/loaderutil.py +26 -7
  12. dissect/target/helpers/network_managers.py +22 -7
  13. dissect/target/helpers/record.py +37 -0
  14. dissect/target/helpers/record_modifier.py +23 -4
  15. dissect/target/helpers/shell_application_ids.py +732 -0
  16. dissect/target/helpers/utils.py +11 -0
  17. dissect/target/loader.py +1 -0
  18. dissect/target/loaders/ab.py +285 -0
  19. dissect/target/loaders/libvirt.py +40 -0
  20. dissect/target/loaders/mqtt.py +14 -1
  21. dissect/target/loaders/tar.py +8 -4
  22. dissect/target/loaders/utm.py +3 -0
  23. dissect/target/loaders/velociraptor.py +6 -6
  24. dissect/target/plugin.py +60 -3
  25. dissect/target/plugins/apps/browser/chrome.py +1 -0
  26. dissect/target/plugins/apps/browser/chromium.py +7 -5
  27. dissect/target/plugins/apps/browser/edge.py +1 -0
  28. dissect/target/plugins/apps/browser/firefox.py +82 -36
  29. dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
  30. dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
  31. dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
  32. dissect/target/plugins/apps/ssh/openssh.py +1 -1
  33. dissect/target/plugins/apps/ssh/ssh.py +177 -0
  34. dissect/target/plugins/apps/texteditor/__init__.py +0 -0
  35. dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
  36. dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
  37. dissect/target/plugins/child/qemu.py +21 -0
  38. dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
  39. dissect/target/plugins/filesystem/unix/capability.py +102 -87
  40. dissect/target/plugins/filesystem/walkfs.py +32 -21
  41. dissect/target/plugins/filesystem/yara.py +144 -23
  42. dissect/target/plugins/general/network.py +82 -0
  43. dissect/target/plugins/general/users.py +14 -10
  44. dissect/target/plugins/os/unix/_os.py +19 -5
  45. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
  46. dissect/target/plugins/os/unix/esxi/_os.py +29 -23
  47. dissect/target/plugins/os/unix/etc/etc.py +5 -8
  48. dissect/target/plugins/os/unix/history.py +3 -7
  49. dissect/target/plugins/os/unix/linux/_os.py +15 -14
  50. dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
  51. dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
  52. dissect/target/plugins/os/unix/locale.py +17 -6
  53. dissect/target/plugins/os/unix/shadow.py +47 -31
  54. dissect/target/plugins/os/windows/_os.py +4 -4
  55. dissect/target/plugins/os/windows/adpolicy.py +4 -1
  56. dissect/target/plugins/os/windows/catroot.py +1 -11
  57. dissect/target/plugins/os/windows/credential/__init__.py +0 -0
  58. dissect/target/plugins/os/windows/credential/lsa.py +174 -0
  59. dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
  60. dissect/target/plugins/os/windows/defender.py +6 -3
  61. dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
  62. dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
  63. dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
  64. dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
  65. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
  66. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
  67. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
  68. dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
  69. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
  70. dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
  71. dissect/target/plugins/os/windows/jumplist.py +292 -0
  72. dissect/target/plugins/os/windows/lnk.py +96 -93
  73. dissect/target/plugins/os/windows/regf/shellbags.py +8 -5
  74. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  75. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  76. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  77. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  78. dissect/target/target.py +4 -3
  79. dissect/target/tools/fs.py +53 -15
  80. dissect/target/tools/fsutils.py +243 -0
  81. dissect/target/tools/info.py +11 -4
  82. dissect/target/tools/query.py +2 -2
  83. dissect/target/tools/shell.py +505 -333
  84. dissect/target/tools/utils.py +23 -2
  85. dissect/target/tools/yara.py +65 -0
  86. dissect/target/volumes/md.py +2 -2
  87. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  88. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
  89. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  90. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  91. dissect/target/helpers/ssh.py +0 -177
  92. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  93. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  94. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  95. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -171,7 +171,7 @@ class ConfigurationParser:
171
171
  try:
172
172
  self.parse_file(fh)
173
173
  except Exception as e:
174
- raise ConfigurationParsingError(*e.args) from e
174
+ raise ConfigurationParsingError(e.args) from e
175
175
 
176
176
  if self.collapse_all or self.collapse:
177
177
  self.parsed_data = self._collapse_dict(self.parsed_data)
@@ -240,6 +240,33 @@ class Default(ConfigurationParser):
240
240
  self.parsed_data = information_dict
241
241
 
242
242
 
243
+ class CSVish(Default):
244
+ """Parses CSV-ish config files (does not confirm to CSV standard!)"""
245
+
246
+ def __init__(self, *args, fields: tuple[str], **kwargs) -> None:
247
+ self.fields = fields
248
+ self.num_fields = len(self.fields)
249
+ self.maxsplit = self.num_fields - 1
250
+ super().__init__(*args, **kwargs)
251
+
252
+ def parse_file(self, fh: TextIO) -> None:
253
+ information_dict = {}
254
+
255
+ for i, raw_line in enumerate(self.line_reader(fh, strip_comments=True)):
256
+ line = raw_line.strip()
257
+ columns = re.split(self.SEPARATOR, line, maxsplit=self.maxsplit)
258
+
259
+ if len(columns) < self.num_fields:
260
+ # keep unparsed lines separate (often env vars)
261
+ data = {"line": line}
262
+ else:
263
+ data = dict(zip(self.fields, columns))
264
+
265
+ information_dict[str(i)] = data
266
+
267
+ self.parsed_data = information_dict
268
+
269
+
243
270
  class Ini(ConfigurationParser):
244
271
  """Parses an ini file according using the built-in python ConfigParser"""
245
272
 
@@ -688,11 +715,12 @@ class ParserConfig:
688
715
  collapse_inverse: Optional[bool] = None
689
716
  separator: Optional[tuple[str]] = None
690
717
  comment_prefixes: Optional[tuple[str]] = None
718
+ fields: Optional[tuple[str]] = None
691
719
 
692
720
  def create_parser(self, options: Optional[ParserOptions] = None) -> ConfigurationParser:
693
721
  kwargs = {}
694
722
 
695
- for field_name in ["collapse", "collapse_inverse", "separator", "comment_prefixes"]:
723
+ for field_name in ["collapse", "collapse_inverse", "separator", "comment_prefixes", "fields"]:
696
724
  value = getattr(options, field_name, None) or getattr(self, field_name)
697
725
  if value:
698
726
  kwargs.update({field_name: value})
@@ -721,6 +749,7 @@ CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
721
749
  "toml": ParserConfig(Toml),
722
750
  }
723
751
 
752
+
724
753
  KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
725
754
  "ulogd.conf": ParserConfig(Ini),
726
755
  "sshd_config": ParserConfig(Indentation, separator=(r"\s",)),
@@ -730,6 +759,41 @@ KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
730
759
  "nsswitch.conf": ParserConfig(Default, separator=(":",)),
731
760
  "lsb-release": ParserConfig(Default),
732
761
  "catalog": ParserConfig(Xml),
762
+ "fstab": ParserConfig(
763
+ CSVish,
764
+ separator=(r"\s",),
765
+ comment_prefixes=("#",),
766
+ fields=("device", "mount", "type", "options", "dump", "pass"),
767
+ ),
768
+ "crontab": ParserConfig(
769
+ CSVish,
770
+ separator=(r"\s",),
771
+ comment_prefixes=("#",),
772
+ fields=("minute", "hour", "day", "month", "weekday", "user", "command"),
773
+ ),
774
+ "shadow": ParserConfig(
775
+ CSVish,
776
+ separator=(r"\:",),
777
+ comment_prefixes=("#",),
778
+ fields=(
779
+ "username",
780
+ "password",
781
+ "lastchange",
782
+ "minpassage",
783
+ "maxpassage",
784
+ "warning",
785
+ "inactive",
786
+ "expire",
787
+ "rest",
788
+ ),
789
+ ),
790
+ "passwd": ParserConfig(
791
+ CSVish,
792
+ separator=(r"\:",),
793
+ comment_prefixes=("#",),
794
+ fields=("username", "password", "uid", "gid", "gecos", "homedir", "shell"),
795
+ ),
796
+ "mime.types": ParserConfig(CSVish, separator=(r"\s+",), comment_prefixes=("#",), fields=("name", "extensions")),
733
797
  }
734
798
 
735
799
 
@@ -748,13 +812,13 @@ def parse(path: Union[FilesystemEntry, TargetPath], hint: Optional[str] = None,
748
812
  FileNotFoundError: If the ``path`` is not a file.
749
813
  """
750
814
 
751
- if not path.is_file(follow_symlinks=True):
752
- raise FileNotFoundError(f"Could not parse {path} as a dictionary.")
753
-
754
815
  entry = path
755
816
  if isinstance(path, TargetPath):
756
817
  entry = path.get()
757
818
 
819
+ if not entry.is_file(follow_symlinks=True):
820
+ raise FileNotFoundError(f"Could not parse {path} as a dictionary.")
821
+
758
822
  options = ParserOptions(*args, **kwargs)
759
823
 
760
824
  return parse_config(entry, hint, options)
@@ -137,7 +137,7 @@ def nms(buf: str, color: Optional[Color] = None, mask_space: bool = False, mask_
137
137
 
138
138
  if (
139
139
  ("\n" in char or "\r\n" in char)
140
- or (not mask_space and char == " " and not is_indent and not mask_indent)
140
+ or (not mask_space and char == " " and not is_indent)
141
141
  or (not mask_indent and is_indent)
142
142
  ):
143
143
  if "\n" in char:
@@ -189,7 +189,7 @@ def nms(buf: str, color: Optional[Color] = None, mask_space: bool = False, mask_
189
189
 
190
190
  if (
191
191
  ("\n" in char or "\r\n" in char)
192
- or (not mask_space and char == " " and not is_indent and not mask_indent)
192
+ or (not mask_space and char == " " and not is_indent)
193
193
  or (not mask_indent and is_indent)
194
194
  ):
195
195
  if "\n" in char:
@@ -268,6 +268,8 @@ def matrix(buf: str, color: Optional[Color] = None, **kwargs) -> None:
268
268
 
269
269
  if cur_ansi:
270
270
  char = cur_ansi + char + "\033[0m"
271
+ if end_ansi:
272
+ cur_ansi = ""
271
273
 
272
274
  if "\n" in char or "\r\n" in char:
273
275
  char = " " + char
@@ -96,6 +96,7 @@ __all__ = [
96
96
  "TargetPath",
97
97
  "walk_ext",
98
98
  "walk",
99
+ "recurse",
99
100
  ]
100
101
 
101
102
 
@@ -144,6 +145,7 @@ class stat_result: # noqa
144
145
  "st_file_attributes": "Windows file attribute bits",
145
146
  "st_fstype": "Type of filesystem",
146
147
  "st_reparse_tag": "Windows reparse tag",
148
+ "st_birthtime_ns": "time of creation in nanoseconds",
147
149
  # Internal fields
148
150
  "_s": "internal tuple",
149
151
  }
@@ -193,6 +195,7 @@ class stat_result: # noqa
193
195
  self.st_file_attributes = s[22]
194
196
  self.st_fstype = s[23]
195
197
  self.st_reparse_tag = s[24]
198
+ self.st_birthtime_ns = s[25]
196
199
 
197
200
  # stat_result behaves like a tuple, but only with the first 10 fields
198
201
  # Note that this means it specifically uses the integer variants of the timestamps
@@ -289,6 +292,20 @@ def walk_ext(path_entry, topdown=True, onerror=None, followlinks=False):
289
292
  yield [path_entry], dirs, files
290
293
 
291
294
 
295
+ def recurse(path_entry: filesystem.FilesystemEntry) -> Iterator[filesystem.FilesystemEntry]:
296
+ """Recursively walk the given :class:`FilesystemEntry`, yields :class:`FilesystemEntry` instances."""
297
+ yield path_entry
298
+
299
+ if not path_entry.is_dir():
300
+ return
301
+
302
+ for child_entry in path_entry.scandir():
303
+ if child_entry.is_dir() and not child_entry.is_symlink():
304
+ yield from recurse(child_entry)
305
+ else:
306
+ yield child_entry
307
+
308
+
292
309
  def glob_split(pattern: str, alt_separator: str = "") -> tuple[str, str]:
293
310
  """Split a pattern on path part boundaries on the first path part with a glob pattern.
294
311
 
@@ -423,15 +440,20 @@ def has_glob_magic(s) -> bool:
423
440
 
424
441
 
425
442
  def resolve_link(
426
- fs: filesystem.Filesystem, entry: filesystem.FilesystemEntry, previous_links: set[str] = None
443
+ fs: filesystem.Filesystem,
444
+ link: str,
445
+ path: str,
446
+ *,
447
+ alt_separator: str = "",
448
+ previous_links: set[str] | None = None,
427
449
  ) -> filesystem.FilesystemEntry:
428
450
  """Resolves a symlink to its actual path.
429
451
 
430
452
  It stops resolving once it detects an infinite recursion loop.
431
453
  """
432
454
 
433
- link = normalize(entry.readlink(), alt_separator=entry.fs.alt_separator)
434
- path = normalize(entry.path, alt_separator=entry.fs.alt_separator)
455
+ link = normalize(link, alt_separator=alt_separator)
456
+ path = normalize(path, alt_separator=alt_separator)
435
457
 
436
458
  # Create hash for entry based on path and link
437
459
  link_id = f"{path}{link}"
@@ -454,7 +476,13 @@ def resolve_link(
454
476
  entry = fs.get(link)
455
477
 
456
478
  if entry.is_symlink():
457
- entry = resolve_link(fs, entry, previous_links)
479
+ entry = resolve_link(
480
+ fs,
481
+ entry.readlink(),
482
+ link,
483
+ alt_separator=entry.fs.alt_separator,
484
+ previous_links=previous_links,
485
+ )
458
486
 
459
487
  return entry
460
488
 
@@ -5,7 +5,7 @@ import re
5
5
  import urllib
6
6
  from os import PathLike
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, BinaryIO, Optional, Union
8
+ from typing import TYPE_CHECKING, BinaryIO
9
9
 
10
10
  from dissect.target.exceptions import FileNotFoundError
11
11
  from dissect.target.filesystem import Filesystem
@@ -42,12 +42,31 @@ def add_virtual_ntfs_filesystem(
42
42
  fh_sds = _try_open(fs, sds_path)
43
43
 
44
44
  if any([fh_boot, fh_mft]):
45
- ntfs = NtfsFilesystem(boot=fh_boot, mft=fh_mft, usnjrnl=fh_usnjrnl, sds=fh_sds)
46
- target.filesystems.add(ntfs)
47
- fs.ntfs = ntfs.ntfs
45
+ ntfs = None
48
46
 
49
-
50
- def _try_open(fs: Filesystem, path: str) -> BinaryIO:
47
+ try:
48
+ ntfs = NtfsFilesystem(boot=fh_boot, mft=fh_mft, usnjrnl=fh_usnjrnl, sds=fh_sds)
49
+ except Exception as e:
50
+ if fh_boot:
51
+ log.warning("Failed to load NTFS filesystem from %s, retrying without $Boot file", fs)
52
+ log.debug("", exc_info=e)
53
+
54
+ try:
55
+ # Try once more without the $Boot file
56
+ ntfs = NtfsFilesystem(mft=fh_mft, usnjrnl=fh_usnjrnl, sds=fh_sds)
57
+ except Exception:
58
+ log.warning("Failed to load NTFS filesystem from %s without $Boot file, skipping", fs)
59
+ return
60
+
61
+ # Only add it if we have a valid NTFS with an MFT
62
+ if ntfs and ntfs.ntfs.mft:
63
+ target.filesystems.add(ntfs)
64
+ fs.ntfs = ntfs.ntfs
65
+ else:
66
+ log.warning("Opened NTFS filesystem from %s but could not find $MFT, skipping", fs)
67
+
68
+
69
+ def _try_open(fs: Filesystem, path: str) -> BinaryIO | None:
51
70
  paths = [path] if not isinstance(path, list) else path
52
71
 
53
72
  for path in paths:
@@ -61,7 +80,7 @@ def _try_open(fs: Filesystem, path: str) -> BinaryIO:
61
80
  pass
62
81
 
63
82
 
64
- def extract_path_info(path: Union[str, Path]) -> tuple[Path, Optional[urllib.parse.ParseResult]]:
83
+ def extract_path_info(path: str | Path) -> tuple[Path, urllib.parse.ParseResult | None]:
65
84
  """
66
85
  Extracts a ParseResult from a path if it has
67
86
  a scheme and adjusts the path if necessary.
@@ -7,12 +7,14 @@ from configparser import ConfigParser, MissingSectionHeaderError
7
7
  from io import StringIO
8
8
  from itertools import chain
9
9
  from re import compile, sub
10
- from typing import Any, Callable, Iterable, Match, Optional
10
+ from typing import Any, Callable, Iterable, Iterator, Match, Optional
11
11
 
12
12
  from defusedxml import ElementTree
13
13
 
14
14
  from dissect.target.exceptions import PluginError
15
15
  from dissect.target.helpers.fsutil import TargetPath
16
+ from dissect.target.plugins.os.unix.log.journal import JournalRecord
17
+ from dissect.target.plugins.os.unix.log.messages import MessagesRecord
16
18
  from dissect.target.target import Target
17
19
 
18
20
  log = logging.getLogger(__name__)
@@ -509,14 +511,15 @@ class LinuxNetworkManager:
509
511
  return values
510
512
 
511
513
 
512
- def parse_unix_dhcp_log_messages(target) -> list[str]:
514
+ def parse_unix_dhcp_log_messages(target: Target, iter_all: bool = False) -> set[str]:
513
515
  """Parse local syslog, journal and cloud init-log files for DHCP lease IPs.
514
516
 
515
517
  Args:
516
518
  target: Target to discover and obtain network information from.
519
+ iter_all: Parse limited amount of journal messages (first 10000) or all of them.
517
520
 
518
521
  Returns:
519
- List of DHCP ip addresses.
522
+ A set of found DHCP IP addresses.
520
523
  """
521
524
  ips = set()
522
525
  messages = set()
@@ -530,9 +533,19 @@ def parse_unix_dhcp_log_messages(target) -> list[str]:
530
533
  if not messages:
531
534
  target.log.warning(f"Could not search for DHCP leases using {log_func}: No log entries found.")
532
535
 
533
- for record in messages:
536
+ def records_enumerate(iterable: Iterable) -> Iterator[tuple[int, JournalRecord | MessagesRecord]]:
537
+ count = 0
538
+ for rec in iterable:
539
+ if rec._desc.name == "linux/log/journal":
540
+ count += 1
541
+ yield count, rec
542
+
543
+ for count, record in records_enumerate(messages):
534
544
  line = record.message
535
545
 
546
+ if not line:
547
+ continue
548
+
536
549
  # Ubuntu cloud-init
537
550
  if "Received dhcp lease on" in line:
538
551
  interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
@@ -576,9 +589,11 @@ def parse_unix_dhcp_log_messages(target) -> list[str]:
576
589
  ips.add(ip)
577
590
  continue
578
591
 
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":
592
+ # The journal parser is relatively slow, so we stop when we have read 10000 journal entries,
593
+ # or if we have found at least one ip address. When `iter_all` is `True` we continue searching.
594
+ if not iter_all and (ips or count > 10_000):
595
+ if not ips:
596
+ target.log.warning("No DHCP IP addresses found in first 10000 journal entries.")
582
597
  break
583
598
 
584
599
  return ips
@@ -142,3 +142,40 @@ EmptyRecord = RecordDescriptor(
142
142
  "empty",
143
143
  [],
144
144
  )
145
+
146
+ COMMON_INTERFACE_ELEMENTS = [
147
+ ("string", "name"),
148
+ ("string", "type"),
149
+ ("boolean", "enabled"),
150
+ ("string", "mac"),
151
+ ("net.ipaddress[]", "dns"),
152
+ ("net.ipaddress[]", "ip"),
153
+ ("net.ipaddress[]", "gateway"),
154
+ ("string", "source"),
155
+ ]
156
+
157
+
158
+ UnixInterfaceRecord = TargetRecordDescriptor(
159
+ "unix/network/interface",
160
+ COMMON_INTERFACE_ELEMENTS,
161
+ )
162
+
163
+ WindowsInterfaceRecord = TargetRecordDescriptor(
164
+ "windows/network/interface",
165
+ [
166
+ *COMMON_INTERFACE_ELEMENTS,
167
+ ("varint", "vlan"),
168
+ ("string", "metric"),
169
+ ("datetime", "last_connected"),
170
+ ],
171
+ )
172
+
173
+ MacInterfaceRecord = TargetRecordDescriptor(
174
+ "macos/network/interface",
175
+ [
176
+ *COMMON_INTERFACE_ELEMENTS,
177
+ ("varint", "vlan"),
178
+ ("string", "proxy"),
179
+ ("varint", "interface_service_order"),
180
+ ],
181
+ )
@@ -4,7 +4,7 @@ from typing import Callable, Iterable, Iterator
4
4
  from flow.record import GroupedRecord, Record, RecordDescriptor, fieldtypes
5
5
 
6
6
  from dissect.target import Target
7
- from dissect.target.exceptions import FilesystemError
7
+ from dissect.target.exceptions import FileNotFoundError, FilesystemError
8
8
  from dissect.target.helpers.fsutil import TargetPath
9
9
  from dissect.target.helpers.hashutil import common
10
10
  from dissect.target.helpers.utils import StrEnum
@@ -44,7 +44,20 @@ def _resolve_path_records(field_name: str, resolved_path: TargetPath) -> Record:
44
44
 
45
45
 
46
46
  def _hash_path_records(field_name: str, resolved_path: TargetPath) -> Record:
47
- """Hash files from path fields inside the record."""
47
+ """Hash files from path fields inside the record.
48
+
49
+ Args:
50
+ field_name: Name of the field.
51
+ resolved_path: Path to the file we should hash.
52
+
53
+ Raises:
54
+ FileNotFoundError: Raised if the provided ``resolved_path`` does not exist or is not a file on the target.
55
+
56
+ Returns: Modified record with digests of path field types.
57
+ """
58
+
59
+ if not resolved_path.exists() or not resolved_path.is_file():
60
+ raise FileNotFoundError(f"Path not found or is not a file: '{resolved_path}'")
48
61
 
49
62
  with resolved_path.open() as fh:
50
63
  path_hash = common(fh)
@@ -81,8 +94,14 @@ def modify_record(target: Target, record: Record, modifier_function: ModifierFun
81
94
  for field_name, resolved_path in _resolve_path_types(target, record):
82
95
  try:
83
96
  _record = modifier_function(field_name, resolved_path)
84
- except FilesystemError:
85
- pass
97
+ except FilesystemError as e:
98
+ target.log.warning(
99
+ "Unable to modify record '%s' with function '%s': %s",
100
+ record._desc.name,
101
+ modifier_function.__name__,
102
+ e,
103
+ )
104
+ target.log.debug("", exc_info=e)
86
105
  else:
87
106
  additional_records.append(_record)
88
107