dissect.target 3.20.dev26__py3-none-any.whl → 3.20.dev28__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.
- dissect/target/filesystems/config.py +4 -0
- dissect/target/helpers/configutil.py +97 -3
- dissect/target/plugins/apps/other/env.py +56 -0
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/METADATA +1 -1
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/RECORD +10 -9
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/LICENSE +0 -0
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/WHEEL +0 -0
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/top_level.txt +0 -0
@@ -247,10 +247,14 @@ class ConfigurationEntry(FilesystemEntry):
|
|
247
247
|
Returns:
|
248
248
|
A file-like object holding a byte representation of :attr:`parser_items`.
|
249
249
|
"""
|
250
|
+
|
250
251
|
if isinstance(self.parser_items, ConfigurationParser):
|
251
252
|
# Currently trying to open the underlying entry
|
252
253
|
return self.entry.open()
|
253
254
|
|
255
|
+
if isinstance(self.parser_items, bytes):
|
256
|
+
return io.BytesIO(self.parser_items)
|
257
|
+
|
254
258
|
output_data = self._write_value_mapping(self.parser_items)
|
255
259
|
return io.BytesIO(bytes(output_data, "utf-8"))
|
256
260
|
|
@@ -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):
|
@@ -161,7 +165,7 @@ class ConfigurationParser:
|
|
161
165
|
def get(self, item: str, default: Optional[Any] = None) -> Any:
|
162
166
|
return self.parsed_data.get(item, default)
|
163
167
|
|
164
|
-
def read_file(self, fh: TextIO) -> None:
|
168
|
+
def read_file(self, fh: TextIO | io.BytesIO) -> None:
|
165
169
|
"""Parse a configuration file.
|
166
170
|
|
167
171
|
Raises:
|
@@ -303,6 +307,14 @@ class Txt(ConfigurationParser):
|
|
303
307
|
self.parsed_data = {"content": fh.read(), "size": str(fh.tell())}
|
304
308
|
|
305
309
|
|
310
|
+
class Bin(ConfigurationParser):
|
311
|
+
|
312
|
+
"""Read the file into ``binary`` and show the number of bytes read"""
|
313
|
+
|
314
|
+
def parse_file(self, fh: io.BytesIO) -> None:
|
315
|
+
self.parsed_data = {"binary": fh.read(), "size": str(fh.tell())}
|
316
|
+
|
317
|
+
|
306
318
|
class Xml(ConfigurationParser):
|
307
319
|
"""Parses an XML file. Ignores any constructor parameters passed from ``ConfigurationParser`."""
|
308
320
|
|
@@ -457,6 +469,76 @@ class Toml(ConfigurationParser):
|
|
457
469
|
raise ConfigurationParsingError("Failed to parse file, please install tomli.")
|
458
470
|
|
459
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
|
+
|
460
542
|
class ScopeManager:
|
461
543
|
"""A (context)manager for dictionary scoping.
|
462
544
|
|
@@ -733,6 +815,8 @@ MATCH_MAP: dict[str, ParserConfig] = {
|
|
733
815
|
"*/sysconfig/network-scripts/ifcfg-*": ParserConfig(Default),
|
734
816
|
"*/sysctl.d/*.conf": ParserConfig(Default),
|
735
817
|
"*/xml/*": ParserConfig(Xml),
|
818
|
+
"*.bashrc": ParserConfig(Txt),
|
819
|
+
"*/vim/vimrc*": ParserConfig(Txt),
|
736
820
|
}
|
737
821
|
|
738
822
|
CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
|
@@ -744,6 +828,13 @@ CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
|
|
744
828
|
"cnf": ParserConfig(Default),
|
745
829
|
"conf": ParserConfig(Default, separator=(r"\s",)),
|
746
830
|
"sample": ParserConfig(Txt),
|
831
|
+
"sh": ParserConfig(Txt),
|
832
|
+
"key": ParserConfig(Txt),
|
833
|
+
"crt": ParserConfig(Txt),
|
834
|
+
"pem": ParserConfig(Txt),
|
835
|
+
"pl": ParserConfig(Txt), # various admin panels
|
836
|
+
"lua": ParserConfig(Txt), # wireshark etc.
|
837
|
+
"txt": ParserConfig(Txt),
|
747
838
|
"systemd": ParserConfig(SystemD),
|
748
839
|
"template": ParserConfig(Txt),
|
749
840
|
"toml": ParserConfig(Toml),
|
@@ -759,6 +850,7 @@ KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
|
|
759
850
|
"nsswitch.conf": ParserConfig(Default, separator=(":",)),
|
760
851
|
"lsb-release": ParserConfig(Default),
|
761
852
|
"catalog": ParserConfig(Xml),
|
853
|
+
"ld.so.cache": ParserConfig(Bin),
|
762
854
|
"fstab": ParserConfig(
|
763
855
|
CSVish,
|
764
856
|
separator=(r"\s",),
|
@@ -832,9 +924,11 @@ def parse_config(
|
|
832
924
|
parser_type = _select_parser(entry, hint)
|
833
925
|
|
834
926
|
parser = parser_type.create_parser(options)
|
835
|
-
|
836
927
|
with entry.open() as fh:
|
837
|
-
|
928
|
+
if not isinstance(parser, Bin):
|
929
|
+
open_file = io.TextIOWrapper(fh, encoding="utf-8")
|
930
|
+
else:
|
931
|
+
open_file = io.BytesIO(fh.read())
|
838
932
|
parser.read_file(open_file)
|
839
933
|
|
840
934
|
return parser
|
@@ -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.
|
3
|
+
Version: 3.20.dev28
|
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
|
@@ -25,7 +25,7 @@ dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
25
25
|
dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
|
26
26
|
dissect/target/filesystems/btrfs.py,sha256=TotOs0-VOmgSijERZb1pOrIH_E7B1J_DRKqws8ttQTk,6569
|
27
27
|
dissect/target/filesystems/cb.py,sha256=6LcoJiwsYu1Han31IUzVpZVDTifhTLTx_gLfNpB_p6k,5329
|
28
|
-
dissect/target/filesystems/config.py,sha256=
|
28
|
+
dissect/target/filesystems/config.py,sha256=5ZJfxs1Cidjxr7nKH2_iGKNWFd5SeLORRWkL4oYcZnk,12063
|
29
29
|
dissect/target/filesystems/cpio.py,sha256=ssVCjkAtLn2FqmNxeo6U5boyUdSYFxLWfXpytHYGPqs,641
|
30
30
|
dissect/target/filesystems/dir.py,sha256=rKEreX3A7CI6a3pMssrO9F-9i5pkxCn_Ucs_dMtHxxA,4574
|
31
31
|
dissect/target/filesystems/exfat.py,sha256=PRkZPUVN5NlgB1VetFtywdNgF6Yj5OBtF5a25t-fFvw,5917
|
@@ -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=
|
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
|
@@ -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.
|
372
|
-
dissect.target-3.20.
|
373
|
-
dissect.target-3.20.
|
374
|
-
dissect.target-3.20.
|
375
|
-
dissect.target-3.20.
|
376
|
-
dissect.target-3.20.
|
377
|
-
dissect.target-3.20.
|
372
|
+
dissect.target-3.20.dev28.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
373
|
+
dissect.target-3.20.dev28.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
374
|
+
dissect.target-3.20.dev28.dist-info/METADATA,sha256=KaInzlw-bjxrdEkeFM3pb6BDAM8IBEP6atLJ3wjH_d4,12897
|
375
|
+
dissect.target-3.20.dev28.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
376
|
+
dissect.target-3.20.dev28.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
|
377
|
+
dissect.target-3.20.dev28.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
378
|
+
dissect.target-3.20.dev28.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.20.dev26.dist-info → dissect.target-3.20.dev28.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|