dissect.target 3.19.dev21__py3-none-any.whl → 3.19.dev22__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.
@@ -1,14 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ from io import BytesIO
1
6
  from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ from dissect.target.helpers import hashutil
2
10
 
3
11
  try:
4
12
  import yara
13
+
14
+ HAS_YARA = True
15
+
5
16
  except ImportError:
6
- raise ImportError("Please install 'yara-python' to use 'target-query -f yara'.")
17
+ HAS_YARA = False
7
18
 
8
- from dissect.target.exceptions import FileNotFoundError
19
+ from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
9
20
  from dissect.target.helpers.record import TargetRecordDescriptor
10
21
  from dissect.target.plugin import Plugin, arg, export
11
22
 
23
+ log = logging.getLogger(__name__)
24
+
12
25
  YaraMatchRecord = TargetRecordDescriptor(
13
26
  "filesystem/yara/match",
14
27
  [
@@ -16,48 +29,158 @@ YaraMatchRecord = TargetRecordDescriptor(
16
29
  ("digest", "digest"),
17
30
  ("string", "rule"),
18
31
  ("string[]", "tags"),
32
+ ("string", "namespace"),
19
33
  ],
20
34
  )
21
35
 
36
+ DEFAULT_MAX_SCAN_SIZE = 10 * 1024 * 1024
37
+
22
38
 
23
39
  class YaraPlugin(Plugin):
24
40
  """Plugin to scan files against a local YARA rules file."""
25
41
 
26
- DEFAULT_MAX_SIZE = 10 * 1024 * 1024
27
-
28
42
  def check_compatible(self) -> None:
29
- pass
43
+ if not HAS_YARA:
44
+ raise UnsupportedPluginError("Please install 'yara-python' to use the yara plugin.")
30
45
 
31
- @arg("--rule-files", "-r", type=Path, nargs="+", required=True, help="path to YARA rule file")
32
- @arg("--scan-path", default="/", help="path to recursively scan")
33
- @arg("--max-size", "-m", default=DEFAULT_MAX_SIZE, help="maximum file size in bytes to scan")
46
+ @arg("-r", "--rules", required=True, nargs="*", help="path(s) to YARA rule file(s) or folder(s)")
47
+ @arg("-p", "--path", default="/", help="path on target(s) to recursively scan")
48
+ @arg("-m", "--max-size", default=DEFAULT_MAX_SCAN_SIZE, help="maximum file size in bytes to scan")
49
+ @arg("-c", "--check", default=False, action="store_true", help="check if every YARA rule is valid")
34
50
  @export(record=YaraMatchRecord)
35
- def yara(self, rule_files, scan_path="/", max_size=DEFAULT_MAX_SIZE):
36
- """Scan files up to a given maximum size with a local YARA rule file.
51
+ def yara(
52
+ self,
53
+ rules: list[str | Path],
54
+ path: str = "/",
55
+ max_size: int = DEFAULT_MAX_SCAN_SIZE,
56
+ check: bool = False,
57
+ ) -> Iterator[YaraMatchRecord]:
58
+ """Scan files inside the target up to a given maximum size with YARA rule file(s).
59
+
60
+ Args:
61
+ rules: ``list`` of strings or ``Path`` objects pointing to rule files to use.
62
+ path: ``string`` of absolute target path to scan.
63
+ max_size: Files larger than this size will not be scanned.
64
+ check: Check if provided rules are valid, only compiles valid rules.
37
65
 
38
- Example:
39
- target-query <TARGET> -f yara --rule-file /path/to/yara_sigs.rule
66
+ Returns:
67
+ Iterator yields ``YaraMatchRecord``.
40
68
  """
41
69
 
42
- rule_data = "\n".join([rule_file.read_text() for rule_file in rule_files])
70
+ compiled_rules = process_rules(rules, check)
43
71
 
44
- rules = yara.compile(source=rule_data)
45
- for _, _, files in self.target.fs.walk_ext(scan_path):
46
- for file_entry in files:
47
- path = self.target.fs.path(file_entry.path)
72
+ if not rules:
73
+ self.target.log.error("No working rules found in '%s'", ",".join(rules))
74
+ return
75
+
76
+ if hasattr(compiled_rules, "warnings") and (num_warns := len(compiled_rules.warnings)) > 0:
77
+ self.target.log.warning("YARA generated %s warnings while compiling rules", num_warns)
78
+ for warning in compiled_rules.warnings:
79
+ self.target.log.debug(warning)
80
+
81
+ self.target.log.warning("Will not scan files larger than %s MB", max_size // 1024 // 1024)
82
+
83
+ for _, _, files in self.target.fs.walk_ext(path):
84
+ for file in files:
48
85
  try:
49
- if path.stat().st_size > max_size:
86
+ if file_size := file.stat().st_size > max_size:
87
+ self.target.log.debug(
88
+ "Skipping file '%s' as it is larger than %s bytes (size is %s)", file, file_size, max_size
89
+ )
50
90
  continue
51
91
 
52
- for match in rules.match(data=path.read_bytes()):
92
+ buf = file.open().read()
93
+ for match in compiled_rules.match(data=buf):
53
94
  yield YaraMatchRecord(
54
- path=path,
55
- digest=path.get().hash(),
95
+ path=self.target.fs.path(file.path),
96
+ digest=hashutil.common(BytesIO(buf)),
56
97
  rule=match.rule,
57
98
  tags=match.tags,
99
+ namespace=match.namespace,
58
100
  _target=self.target,
59
101
  )
102
+
60
103
  except FileNotFoundError:
61
104
  continue
62
- except Exception:
63
- self.target.log.exception("Error scanning file: %s", path)
105
+ except RuntimeWarning as e:
106
+ self.target.log.warning("Runtime warning while scanning file '%s': %s", file, e)
107
+ except Exception as e:
108
+ self.target.log.error("Exception scanning file '%s'", file)
109
+ self.target.log.debug("", exc_info=e)
110
+
111
+
112
+ def process_rules(paths: list[str | Path], check: bool = False) -> yara.Rules | None:
113
+ """Generate compiled YARA rules from the given path(s).
114
+
115
+ Provide path to one (compiled) YARA file or directory containing YARA files.
116
+
117
+ Args:
118
+ paths: Path to file(s) or folder(s) containing YARA files.
119
+ check: Attempt to compile every rule file before appending to rules.
120
+
121
+ Returns:
122
+ Compiled YARA rules or None.
123
+ """
124
+ files = set()
125
+ compiled_rules = None
126
+
127
+ for rules_path in paths:
128
+ if isinstance(rules_path, str):
129
+ rules_path = Path(rules_path)
130
+
131
+ if not rules_path.exists():
132
+ log.warning("File %s does not exist!", rules_path)
133
+ continue
134
+
135
+ if rules_path.is_dir():
136
+ for file in rules_path.rglob("*"):
137
+ if not file.is_file():
138
+ continue
139
+ files.add(file)
140
+ else:
141
+ files.add(rules_path)
142
+
143
+ for file in set(files):
144
+ with file.open("rb") as fh:
145
+ magic = fh.read(4)
146
+
147
+ if magic == b"YARA":
148
+ if len(files) > 1:
149
+ log.error("Providing multiple compiled YARA files is not supported. Did not add %s", file)
150
+ continue
151
+ else:
152
+ log.info("Adding single compiled YARA file %s", file)
153
+ compiled_rules = compile_yara(file, is_compiled=True)
154
+ break
155
+
156
+ elif check and not is_valid_yara({"check_namespace": file}):
157
+ log.warning("File %s contains invalid rule(s)!", file)
158
+ files.remove(file)
159
+ continue
160
+
161
+ if files and not compiled_rules:
162
+ try:
163
+ compiled_rules = compile_yara({hashlib.md5(file.as_posix().encode()).hexdigest(): file for file in files})
164
+ except yara.Error as e:
165
+ log.error("Failed to compile YARA file(s): %s", e)
166
+
167
+ return compiled_rules
168
+
169
+
170
+ def compile_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> yara.Rules | None:
171
+ """Compile or load the given YARA file(s) to rules."""
172
+ if is_compiled and isinstance(files, Path):
173
+ return yara.load(files.as_posix())
174
+ else:
175
+ return yara.compile(filepaths={ns: Path(path).as_posix() for ns, path in files.items()})
176
+
177
+
178
+ def is_valid_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> bool:
179
+ """Determine if the given YARA file(s) compile without errors or warnings."""
180
+ try:
181
+ compile_yara(files, is_compiled)
182
+ return True
183
+
184
+ except (yara.SyntaxError, yara.WarningError, yara.Error) as e:
185
+ log.debug("Rule file(s) '%s' invalid: %s", files, e)
186
+ return False
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import argparse
4
+ import logging
5
+
6
+ from dissect.target import Target
7
+ from dissect.target.exceptions import TargetError
8
+ from dissect.target.plugins.filesystem.yara import HAS_YARA, YaraPlugin
9
+ from dissect.target.tools.query import record_output
10
+ from dissect.target.tools.utils import (
11
+ catch_sigpipe,
12
+ configure_generic_arguments,
13
+ process_generic_arguments,
14
+ )
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ @catch_sigpipe
20
+ def main():
21
+ help_formatter = argparse.ArgumentDefaultsHelpFormatter
22
+ parser = argparse.ArgumentParser(
23
+ description="target-yara",
24
+ fromfile_prefix_chars="@",
25
+ formatter_class=help_formatter,
26
+ )
27
+
28
+ parser.add_argument("targets", metavar="TARGETS", nargs="*", help="Targets to load")
29
+ parser.add_argument("-s", "--strings", default=False, action="store_true", help="print output as string")
30
+
31
+ for args, kwargs in getattr(YaraPlugin.yara, "__args__", []):
32
+ parser.add_argument(*args, **kwargs)
33
+
34
+ configure_generic_arguments(parser)
35
+
36
+ args = parser.parse_args()
37
+ process_generic_arguments(args)
38
+
39
+ if not HAS_YARA:
40
+ log.error("yara-python is not installed: pip install yara-python")
41
+ parser.exit(1)
42
+
43
+ if not args.targets:
44
+ log.error("No targets provided")
45
+ parser.exit(1)
46
+
47
+ try:
48
+ for target in Target.open_all(args.targets):
49
+ target.log.info("Scanning target")
50
+ rs = record_output(args.strings, False)
51
+ for record in target.yara(args.rules, args.path, args.max_size, args.check):
52
+ rs.write(record)
53
+
54
+ except TargetError as e:
55
+ log.error(e)
56
+ log.debug("", exc_info=e)
57
+ parser.exit(1)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.19.dev21
3
+ Version: 3.19.dev22
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
@@ -164,7 +164,7 @@ dissect/target/plugins/filesystem/acquire_hash.py,sha256=OVxI19-Bl1tdqCiFMscFMLm
164
164
  dissect/target/plugins/filesystem/icat.py,sha256=bOMi04IlljnKwxTWTZJKtK7RxKnabFu3WcXyUwzkE-4,4090
165
165
  dissect/target/plugins/filesystem/resolver.py,sha256=HfyASUFV4F9uD-yFXilFpPTORAsRDvdmTvuYHgOaOWg,4776
166
166
  dissect/target/plugins/filesystem/walkfs.py,sha256=e8HEZcV5Wiua26FGWL3xgiQ_PIhcNvGI5KCdsAx2Nmo,2298
167
- dissect/target/plugins/filesystem/yara.py,sha256=q_pbrQArNaWP4ILRzK7VQhukIw16LhUvntoviHmZ38Q,2241
167
+ dissect/target/plugins/filesystem/yara.py,sha256=JdWqbqDBhKrht3fTroqX7NpBU9khEQUWyMcDgLv2l2g,6686
168
168
  dissect/target/plugins/filesystem/ntfs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
169
169
  dissect/target/plugins/filesystem/ntfs/mft.py,sha256=AD3w2FIjDAf8x2KEbBhz2NeOA_lxIAmw353w6J3ObYU,9565
170
170
  dissect/target/plugins/filesystem/ntfs/mft_timeline.py,sha256=vvNFAZbr7s3X2OTYf4ES_L6-XsouTXcTymfxnHfZ1Rw,6791
@@ -333,6 +333,7 @@ dissect/target/tools/query.py,sha256=ONHu2FVomLccikb84qBrlhNmEfRoHYFQMcahk_y2c9A
333
333
  dissect/target/tools/reg.py,sha256=FDsiBBDxjWVUBTRj8xn82vZe-J_d9piM-TKS3PHZCcM,3193
334
334
  dissect/target/tools/shell.py,sha256=_widEuIRqZhYzcFR52NYI8O2aPFm6tG5Uiv-AIrC32U,45155
335
335
  dissect/target/tools/utils.py,sha256=sQizexY3ui5vmWw4KOBLg5ecK3TPFjD-uxDqRn56ZTY,11304
336
+ dissect/target/tools/yara.py,sha256=xIom_n78oBiDg6VEBMVk8qmvhYMOPzY5yv9Vl1rDbB4,1754
336
337
  dissect/target/tools/dump/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
337
338
  dissect/target/tools/dump/run.py,sha256=aD84peRS4zHqC78fH7Vd4ni3m1ZmVP70LyMwBRvoDGY,9463
338
339
  dissect/target/tools/dump/state.py,sha256=YYgCff0kZZ-tx27lJlc9LQ7AfoGnLK5Gyi796OnktA8,9205
@@ -345,10 +346,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
345
346
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
346
347
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
347
348
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
348
- dissect.target-3.19.dev21.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
349
- dissect.target-3.19.dev21.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
350
- dissect.target-3.19.dev21.dist-info/METADATA,sha256=EnMvW_6eQ-p-R4fxSi-ZjcQ_MvkH6xnkcW6F0YE7oMo,12719
351
- dissect.target-3.19.dev21.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
352
- dissect.target-3.19.dev21.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
353
- dissect.target-3.19.dev21.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
354
- dissect.target-3.19.dev21.dist-info/RECORD,,
349
+ dissect.target-3.19.dev22.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
350
+ dissect.target-3.19.dev22.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
351
+ dissect.target-3.19.dev22.dist-info/METADATA,sha256=f_1UaUvsl2v75UvJhA2SVyEH2FaQ21oR5byPzLlH6mU,12719
352
+ dissect.target-3.19.dev22.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
353
+ dissect.target-3.19.dev22.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
354
+ dissect.target-3.19.dev22.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
355
+ dissect.target-3.19.dev22.dist-info/RECORD,,
@@ -8,3 +8,4 @@ target-mount = dissect.target.tools.mount:main
8
8
  target-query = dissect.target.tools.query:main
9
9
  target-reg = dissect.target.tools.reg:main
10
10
  target-shell = dissect.target.tools.shell:main
11
+ target-yara = dissect.target.tools.yara:main