dissect.target 3.20.dev27__py3-none-any.whl → 3.20.dev29__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.
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import json
5
+ import logging
5
6
  import re
6
7
  import sys
7
8
  from collections import deque
@@ -47,6 +48,9 @@ except ImportError:
47
48
  HAS_TOML = False
48
49
 
49
50
 
51
+ log = logging.getLogger(__name__)
52
+
53
+
50
54
  def _update_dictionary(current: dict[str, Any], key: str, value: Any) -> None:
51
55
  if prev_value := current.get(key):
52
56
  if isinstance(prev_value, dict):
@@ -465,6 +469,76 @@ class Toml(ConfigurationParser):
465
469
  raise ConfigurationParsingError("Failed to parse file, please install tomli.")
466
470
 
467
471
 
472
+ class Env(ConfigurationParser):
473
+ """Parses ``.env`` file contents according to Docker and bash specification.
474
+
475
+ Does not apply interpolation of substituted values, eg. ``foo=${bar}`` and does not attempt
476
+ to parse list or dict strings. Does not support dynamic env files, eg. `` foo=`bar` ``. Also
477
+ does not support multi-line key/value assignments (yet).
478
+
479
+ Resources:
480
+ - https://docs.docker.com/compose/environment-variables/variable-interpolation/#env-file-syntax
481
+ - https://github.com/theskumar/python-dotenv/blob/main/src/dotenv/parser.py
482
+ """
483
+
484
+ RE_KV = re.compile(r"^(?P<key>.+?)=(?P<value>(\".+?\")|(\'.+?\')|(.*?))?(?P<comment> \#.+?)?$")
485
+
486
+ def __init__(self, comments: bool = True, *args, **kwargs) -> None:
487
+ super().__init__(*args, **kwargs)
488
+ self.comments = comments
489
+ self.parsed_data: dict | tuple[dict, str | None] = {}
490
+
491
+ def parse_file(self, fh: TextIO) -> None:
492
+ for line in fh.readlines():
493
+ # Blank lines are ignored.
494
+ # Lines beginning with ``#`` are processed as comments and ignored.
495
+ if not line or line[0] == "#" or "=" not in line:
496
+ continue
497
+
498
+ # Each line represents a key-value pair. Values can optionally be quoted.
499
+ # Inline comments for unquoted values must be preceded with a space.
500
+ # Value may be empty.
501
+ match = self.RE_KV.match(line)
502
+
503
+ # Line could be invalid
504
+ if not match:
505
+ log.warning("Could not parse line in %s: '%s'", fh, line)
506
+ continue
507
+
508
+ key = match.groupdict()["key"]
509
+ value = match.groupdict().get("value") or ""
510
+ value = value.strip()
511
+ comment = match.groupdict().get("comment")
512
+ comment = comment.replace(" # ", "", 1) if comment else None
513
+
514
+ # Surrounding whitespace characters are removed, unless quoted.
515
+ if value and ((value[0] == '"' and value[-1] == '"') or (value[0] == "'" and value[-1] == "'")):
516
+ is_quoted = True
517
+ value = value.strip("\"'")
518
+ else:
519
+ is_quoted = False
520
+ value = value.strip()
521
+
522
+ # Unquoted values may start with a quote if they are properly escaped.
523
+ if not is_quoted and value[:2] in ["\\'", '\\"']:
524
+ value = value[1:]
525
+
526
+ # Interpret boolean values
527
+ if value.lower() in ["1", "true"]:
528
+ value = True
529
+ elif value.lower() in ["0", "false"]:
530
+ value = False
531
+
532
+ # Interpret integer values
533
+ if isinstance(value, str) and re.match(r"^[0-9]{1,}$", value):
534
+ value = int(value)
535
+
536
+ if key.strip() in self.parsed_data:
537
+ log.warning("Duplicate environment key '%s' in file %s", key.strip(), fh)
538
+
539
+ self.parsed_data[key.strip()] = (value, comment) if self.comments else value
540
+
541
+
468
542
  class ScopeManager:
469
543
  """A (context)manager for dictionary scoping.
470
544
 
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
 
4
4
  import fnmatch
5
5
  import re
6
- import struct
7
6
  from collections import defaultdict
8
7
  from datetime import datetime
8
+ from enum import IntEnum
9
+ from functools import cached_property
9
10
  from io import BytesIO
10
11
  from pathlib import Path
11
12
  from typing import BinaryIO, Iterator, Optional, TextIO, Union
@@ -28,6 +29,19 @@ ValueType = Union[int, str, bytes, list[str]]
28
29
  """The possible value types that can be returned from the registry."""
29
30
 
30
31
 
32
+ class RegistryValueType(IntEnum):
33
+ NONE = regf.REG_NONE
34
+ SZ = regf.REG_SZ
35
+ EXPAND_SZ = regf.REG_EXPAND_SZ
36
+ BINARY = regf.REG_BINARY
37
+ DWORD = regf.REG_DWORD
38
+ DWORD_BIG_ENDIAN = regf.REG_DWORD_BIG_ENDIAN
39
+ MULTI_SZ = regf.REG_MULTI_SZ
40
+ FULL_RESOURCE_DESCRIPTOR = regf.REG_FULL_RESOURCE_DESCRIPTOR
41
+ RESOURCE_REQUIREMENTS_LIST = regf.REG_RESOURCE_REQUIREMENTS_LIST
42
+ QWORD = regf.REG_QWORD
43
+
44
+
31
45
  class RegistryHive:
32
46
  """Base class for registry hives."""
33
47
 
@@ -405,8 +419,8 @@ class VirtualValue(RegistryValue):
405
419
  return self._value
406
420
 
407
421
  @property
408
- def type(self) -> int:
409
- return None
422
+ def type(self) -> RegistryValueType:
423
+ return RegistryValueType.NONE
410
424
 
411
425
 
412
426
  class HiveCollection(RegistryHive):
@@ -683,8 +697,8 @@ class RegfValue(RegistryValue):
683
697
  return self.kv.value
684
698
 
685
699
  @property
686
- def type(self) -> int:
687
- return self.kv.type
700
+ def type(self) -> RegistryValueType:
701
+ return RegistryValueType(self.kv.type)
688
702
 
689
703
 
690
704
  class RegFlex:
@@ -750,17 +764,22 @@ class RegFlexKey(VirtualKey):
750
764
 
751
765
  class RegFlexValue(VirtualValue):
752
766
  def __init__(self, hive: RegistryHive, name: str, value: ValueType):
753
- self._parsed_value = None
754
767
  super().__init__(hive, name, value)
755
768
 
769
+ @cached_property
770
+ def _parse(self) -> tuple[RegistryValueType, ValueType]:
771
+ return parse_flex_value(self._value)
772
+
756
773
  @property
757
774
  def value(self) -> ValueType:
758
- if not self._parsed_value:
759
- self._parsed_value = parse_flex_value(self._value)
760
- return self._parsed_value
775
+ return self._parse[1]
776
+
777
+ @property
778
+ def type(self) -> RegistryValueType:
779
+ return self._parse[0]
761
780
 
762
781
 
763
- def parse_flex_value(value: str) -> ValueType:
782
+ def parse_flex_value(value: str) -> tuple[RegistryValueType, ValueType]:
764
783
  """Parse values from text registry exports.
765
784
 
766
785
  Args:
@@ -770,31 +789,31 @@ def parse_flex_value(value: str) -> ValueType:
770
789
  NotImplementedError: If ``value`` is not of a supported type for parsing.
771
790
  """
772
791
  if value.startswith('"'):
773
- return value.strip('"')
792
+ return RegistryValueType.SZ, value.strip('"')
774
793
 
775
794
  vtype, _, value = value.partition(":")
776
795
  if vtype == "dword":
777
- return struct.unpack(">i", bytes.fromhex(value))[0]
796
+ return RegistryValueType.DWORD, int.from_bytes(bytes.fromhex(value), "big", signed=True)
778
797
  elif "hex" in vtype:
779
798
  value = bytes.fromhex(value.replace(",", ""))
780
799
  if vtype == "hex":
781
- return value
800
+ return RegistryValueType.BINARY, value
782
801
 
783
802
  # hex(T)
784
803
  # These values match regf type values
785
804
  vtype = int(vtype[4:5], 16)
786
805
  if vtype == regf.REG_NONE:
787
- return value if value else None
806
+ decoded = value if value else None
788
807
  elif vtype == regf.REG_SZ:
789
- return regf.try_decode_sz(value)
808
+ decoded = regf.try_decode_sz(value)
790
809
  elif vtype == regf.REG_EXPAND_SZ:
791
- return regf.try_decode_sz(value)
810
+ decoded = regf.try_decode_sz(value)
792
811
  elif vtype == regf.REG_BINARY:
793
- return value
812
+ decoded = value
794
813
  elif vtype == regf.REG_DWORD:
795
- return struct.unpack("<I", value)[0]
814
+ decoded = int.from_bytes(value, "little", signed=False)
796
815
  elif vtype == regf.REG_DWORD_BIG_ENDIAN:
797
- return struct.unpack(">I", value)[0]
816
+ decoded = int.from_bytes(value, "big", signed=False)
798
817
  elif vtype == regf.REG_MULTI_SZ:
799
818
  d = BytesIO(value)
800
819
 
@@ -806,11 +825,12 @@ def parse_flex_value(value: str) -> ValueType:
806
825
 
807
826
  r.append(s)
808
827
 
809
- return r
828
+ decoded = r
810
829
  elif vtype == regf.REG_QWORD:
811
- return struct.unpack(">Q", value)[0]
830
+ decoded = int.from_bytes(value, "big", signed=False)
812
831
  else:
813
832
  raise NotImplementedError(f"Registry flex value type {vtype}")
833
+ return RegistryValueType(vtype), decoded
814
834
 
815
835
 
816
836
  def has_glob_magic(pattern: str) -> bool:
@@ -0,0 +1,56 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.helpers.configutil import Env
4
+ from dissect.target.helpers.record import TargetRecordDescriptor
5
+ from dissect.target.plugin import Plugin, arg, export
6
+
7
+ EnvironmentFileRecord = TargetRecordDescriptor(
8
+ "application/other/file/environment",
9
+ [
10
+ ("datetime", "ts_mtime"),
11
+ ("string", "key"),
12
+ ("string", "value"),
13
+ ("string", "comment"),
14
+ ("path", "path"),
15
+ ],
16
+ )
17
+
18
+
19
+ class EnvironmentFilePlugin(Plugin):
20
+ """Environment file plugin."""
21
+
22
+ def check_compatible(self) -> None:
23
+ # `--env-path` is provided at runtime
24
+ pass
25
+
26
+ @export(record=EnvironmentFileRecord)
27
+ @arg("--env-path", help="path to scan environment files in")
28
+ @arg("--extension", help="extension of files to scan", default="env")
29
+ def envfile(self, env_path: str, extension: str = "env") -> Iterator[EnvironmentFileRecord]:
30
+ """Yield environment variables found in ``.env`` files at the provided path."""
31
+
32
+ if not env_path:
33
+ self.target.log.error("No ``--path`` provided!")
34
+
35
+ if not (path := self.target.fs.path(env_path)).exists():
36
+ self.target.log.error("Provided path %s does not exist!", path)
37
+
38
+ for file in path.glob("**/*." + extension):
39
+ if not file.is_file():
40
+ continue
41
+
42
+ mtime = file.lstat().st_mtime
43
+
44
+ with file.open("r") as fh:
45
+ parser = Env(comments=True)
46
+ parser.read_file(fh)
47
+
48
+ for key, (value, comment) in parser.parsed_data.items():
49
+ yield EnvironmentFileRecord(
50
+ ts_mtime=mtime,
51
+ key=key,
52
+ value=value,
53
+ comment=comment,
54
+ path=file,
55
+ _target=self.target,
56
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.20.dev27
3
+ Version: 3.20.dev29
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
@@ -46,7 +46,7 @@ dissect/target/filesystems/zip.py,sha256=BeNj23DOYfWuTm5V1V419ViJiMfBrO1VA5gP6rl
46
46
  dissect/target/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  dissect/target/helpers/cache.py,sha256=TXlJBdFRz6V9zKs903am4Yawr0maYw5kZY0RqklDQJM,8568
48
48
  dissect/target/helpers/config.py,sha256=RMHnIuKJHINHiLrvKN3EyA0jFA1o6-pbeaycG8Pgrp8,2596
49
- dissect/target/helpers/configutil.py,sha256=2PjQG-8vsParTANkMox_2cB1_MDxpOURJrUFj5dL3DI,28355
49
+ dissect/target/helpers/configutil.py,sha256=mO2XwhzLhGjFQzg_zC8SNi24CQhye1fhkYlH5Q5HFm8,31365
50
50
  dissect/target/helpers/cyber.py,sha256=WnJlk-HqAETmDAgLq92JPxyDLxvzSoFV_WrO-odVKBI,16805
51
51
  dissect/target/helpers/descriptor_extensions.py,sha256=uT8GwznfDAiIgMM7JKKOY0PXKMv2c0GCqJTCkWFgops,2605
52
52
  dissect/target/helpers/docs.py,sha256=J5U65Y3yOTqxDEZRCdrEmO63XQCeDzOJea1PwPM6Cyc,5146
@@ -62,7 +62,7 @@ dissect/target/helpers/polypath.py,sha256=h8p7m_OCNiQljGwoZh5Aflr9H2ot6CZr6WKq1O
62
62
  dissect/target/helpers/protobuf.py,sha256=b4DsnqrRLrefcDjx7rQno-_LBcwtJXxuKf5RdOegzfE,1537
63
63
  dissect/target/helpers/record.py,sha256=7Se6ZV8cvwEaGSjRd9bKhVnUAn4W4KR2eqP6AbQhTH4,5892
64
64
  dissect/target/helpers/record_modifier.py,sha256=O_Jj7zOi891HIyAYjxxe6LFPYETHdMa5lNjo4NA_T_w,3969
65
- dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhHXIAM,27337
65
+ dissect/target/helpers/regutil.py,sha256=ti-ht2N9UxbMjhUBP2bybY76_dAvbCz0txPBszvSKVw,28171
66
66
  dissect/target/helpers/shell_application_ids.py,sha256=hYxrP-YtHK7ZM0ectJFHfoMB8QUXLbYNKmKXMWLZRlA,38132
67
67
  dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
68
68
  dissect/target/helpers/utils.py,sha256=K3xVq9D0FwIhTBAuiWN8ph7Pq2GABgG3hOz-3AmKuEA,4244
@@ -127,6 +127,7 @@ dissect/target/plugins/apps/browser/firefox.py,sha256=mZBBagFfIdiz9kUyK4Hi989I4g
127
127
  dissect/target/plugins/apps/browser/iexplore.py,sha256=g_xw0toaiyjevxO8g9XPCOqc-CXZp39FVquRhPFGdTE,8801
128
128
  dissect/target/plugins/apps/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
129
129
  dissect/target/plugins/apps/container/docker.py,sha256=LTsZplaECSfO1Ysp_Y-9WsnNocsreu_iHO8fbSif3g0,16221
130
+ dissect/target/plugins/apps/other/env.py,sha256=_I12S_wjyT18WlUJ5cWOy5OTI140AheH6tq743iiyys,1874
130
131
  dissect/target/plugins/apps/remoteaccess/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
132
  dissect/target/plugins/apps/remoteaccess/anydesk.py,sha256=IdijK3F6ppaB_IgKL-xDljlEbb8l9S2U0xSWKqK9xRs,4294
132
133
  dissect/target/plugins/apps/remoteaccess/remoteaccess.py,sha256=DWXkRDVUpFr1icK2fYwSXdZD204Xz0yRuO7rcJOwIwc,825
@@ -368,10 +369,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
368
369
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
369
370
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
370
371
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
371
- dissect.target-3.20.dev27.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
372
- dissect.target-3.20.dev27.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
373
- dissect.target-3.20.dev27.dist-info/METADATA,sha256=O7DsHBHZKJGXZ1bQFS5nypC1x3WA9342Xj88YwbbT4k,12897
374
- dissect.target-3.20.dev27.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
375
- dissect.target-3.20.dev27.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
376
- dissect.target-3.20.dev27.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
377
- dissect.target-3.20.dev27.dist-info/RECORD,,
372
+ dissect.target-3.20.dev29.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
373
+ dissect.target-3.20.dev29.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
374
+ dissect.target-3.20.dev29.dist-info/METADATA,sha256=5QkftvpVFTGaJunkmrOrcd8izTwayNKC5WbTqXhcMn8,12897
375
+ dissect.target-3.20.dev29.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
376
+ dissect.target-3.20.dev29.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
377
+ dissect.target-3.20.dev29.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
378
+ dissect.target-3.20.dev29.dist-info/RECORD,,