dissect.target 3.20.dev27__py3-none-any.whl → 3.20.dev29__py3-none-any.whl

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