yaralyzer 1.0.11__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.
- .yaralyzer.example +65 -0
- CHANGELOG.md +128 -0
- LICENSE +674 -0
- yaralyzer/__init__.py +76 -0
- yaralyzer/bytes_match.py +276 -0
- yaralyzer/config.py +126 -0
- yaralyzer/decoding/bytes_decoder.py +207 -0
- yaralyzer/decoding/decoding_attempt.py +222 -0
- yaralyzer/encoding_detection/character_encodings.py +197 -0
- yaralyzer/encoding_detection/encoding_assessment.py +83 -0
- yaralyzer/encoding_detection/encoding_detector.py +145 -0
- yaralyzer/helpers/bytes_helper.py +268 -0
- yaralyzer/helpers/dict_helper.py +8 -0
- yaralyzer/helpers/file_helper.py +49 -0
- yaralyzer/helpers/list_helper.py +16 -0
- yaralyzer/helpers/rich_text_helper.py +150 -0
- yaralyzer/helpers/string_helper.py +34 -0
- yaralyzer/output/decoding_attempts_table.py +82 -0
- yaralyzer/output/decoding_table_row.py +60 -0
- yaralyzer/output/file_export.py +111 -0
- yaralyzer/output/file_hashes_table.py +82 -0
- yaralyzer/output/regex_match_metrics.py +97 -0
- yaralyzer/output/rich_console.py +114 -0
- yaralyzer/util/argument_parser.py +297 -0
- yaralyzer/util/logging.py +135 -0
- yaralyzer/yara/error.py +90 -0
- yaralyzer/yara/yara_match.py +160 -0
- yaralyzer/yara/yara_rule_builder.py +164 -0
- yaralyzer/yaralyzer.py +304 -0
- yaralyzer-1.0.11.dist-info/LICENSE +674 -0
- yaralyzer-1.0.11.dist-info/METADATA +151 -0
- yaralyzer-1.0.11.dist-info/RECORD +34 -0
- yaralyzer-1.0.11.dist-info/WHEEL +4 -0
- yaralyzer-1.0.11.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Builds bare bones YARA rules to match strings and regex patterns.
|
|
3
|
+
|
|
4
|
+
Example rule string:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
rule Just_A_Piano_Man {
|
|
8
|
+
meta:
|
|
9
|
+
author = "Tim"
|
|
10
|
+
strings:
|
|
11
|
+
$hilton_producer = /Scott.*Storch/
|
|
12
|
+
condition:
|
|
13
|
+
$hilton_producer
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
"""
|
|
17
|
+
import re
|
|
18
|
+
from typing import Literal, Optional
|
|
19
|
+
|
|
20
|
+
import yara
|
|
21
|
+
|
|
22
|
+
from yaralyzer.config import YARALYZE
|
|
23
|
+
from yaralyzer.util.logging import log
|
|
24
|
+
|
|
25
|
+
PatternType = Literal['hex', 'regex']
|
|
26
|
+
YaraModifierType = Literal['ascii', 'fullword', 'nocase', 'wide']
|
|
27
|
+
|
|
28
|
+
HEX = 'hex'
|
|
29
|
+
PATTERN = 'pattern'
|
|
30
|
+
REGEX = 'regex'
|
|
31
|
+
RULE = 'rule'
|
|
32
|
+
UNDERSCORE = '_'
|
|
33
|
+
YARA_REGEX_MODIFIERS = ['nocase', 'ascii', 'wide', 'fullword']
|
|
34
|
+
|
|
35
|
+
SAFE_LABEL_REPLACEMENTS = {
|
|
36
|
+
'/': 'frontslash',
|
|
37
|
+
'\\': 'backslash',
|
|
38
|
+
"'": 'singlequote',
|
|
39
|
+
'"': 'doublequote',
|
|
40
|
+
'`': 'backtick',
|
|
41
|
+
'-': UNDERSCORE,
|
|
42
|
+
' ': UNDERSCORE,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RULE_TEMPLATE = """
|
|
46
|
+
rule {rule_name} {{
|
|
47
|
+
meta:
|
|
48
|
+
author = "The Yaralyzer"
|
|
49
|
+
strings:
|
|
50
|
+
${pattern_label} = {pattern}
|
|
51
|
+
condition:
|
|
52
|
+
${pattern_label}
|
|
53
|
+
}}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
BYTES_RULE_TEMPLATE = """
|
|
57
|
+
rule {rule_name} {{
|
|
58
|
+
meta:
|
|
59
|
+
author = "The Yaralyzer"
|
|
60
|
+
strings:
|
|
61
|
+
${pattern_label} = {{ {bytes_pattern} }}
|
|
62
|
+
condition:
|
|
63
|
+
${pattern_label}
|
|
64
|
+
}}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def yara_rule_string(
|
|
69
|
+
pattern: str,
|
|
70
|
+
pattern_type: PatternType = REGEX,
|
|
71
|
+
rule_name: str = YARALYZE,
|
|
72
|
+
pattern_label: Optional[str] = PATTERN,
|
|
73
|
+
modifier: Optional[YaraModifierType] = None
|
|
74
|
+
) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Build a YARA rule string for a given `pattern`.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
pattern (str): The string or regex pattern to match.
|
|
80
|
+
pattern_type (str): Either `"regex"` or `"hex"`. Default is `"regex"`.
|
|
81
|
+
rule_name (str): The name of the YARA rule. Default is `"YARALYZE"`.
|
|
82
|
+
pattern_label (Optional[str]): The label for the pattern in the YARA rule. Default is `"pattern"`.
|
|
83
|
+
modifier (Optional[str]): Optional regex modifier (e.g. 'nocase', 'ascii', 'wide', 'fullword').
|
|
84
|
+
Only valid if `pattern_type` is `"regex"`.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str: The constructed YARA rule as a string.
|
|
88
|
+
"""
|
|
89
|
+
if not (modifier is None or modifier in YARA_REGEX_MODIFIERS):
|
|
90
|
+
raise TypeError(f"Modifier '{modifier}' is not one of {YARA_REGEX_MODIFIERS}")
|
|
91
|
+
|
|
92
|
+
if pattern_type == REGEX:
|
|
93
|
+
pattern = f"/{pattern}/"
|
|
94
|
+
elif pattern_type == HEX:
|
|
95
|
+
pattern = f"{{{pattern}}}"
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(f"pattern_type must be either '{REGEX}' or '{HEX}'")
|
|
98
|
+
|
|
99
|
+
if modifier:
|
|
100
|
+
pattern += f" {modifier}"
|
|
101
|
+
|
|
102
|
+
rule = RULE_TEMPLATE.format(
|
|
103
|
+
rule_name=rule_name,
|
|
104
|
+
pattern_label=pattern_label,
|
|
105
|
+
pattern=pattern,
|
|
106
|
+
modifier='' if modifier is None else f" {modifier}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
log.debug(f"Built YARA rule: \n{rule}")
|
|
110
|
+
return rule
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_yara_rule(
|
|
114
|
+
pattern: str,
|
|
115
|
+
pattern_type: PatternType = REGEX,
|
|
116
|
+
rule_name: str = YARALYZE,
|
|
117
|
+
pattern_label: Optional[str] = PATTERN,
|
|
118
|
+
modifier: Optional[YaraModifierType] = None
|
|
119
|
+
) -> yara.Rule:
|
|
120
|
+
"""
|
|
121
|
+
Build a compiled `yara.Rule` object.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
pattern (str): The string or regex pattern to match.
|
|
125
|
+
pattern_type (str): Either `"regex"` or `"hex"`. Default is `"regex"`.
|
|
126
|
+
rule_name (str): The name of the YARA rule. Default is `"YARALYZE"`.
|
|
127
|
+
pattern_label (Optional[str]): The label for the pattern in the YARA rule. Default is `"pattern"`.
|
|
128
|
+
modifier (Optional[str]): Optional regex modifier (e.g. 'nocase', 'ascii', 'wide', 'fullword').
|
|
129
|
+
Only valid if `pattern_type` is `"regex"`.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
yara.Rule: Compiled YARA rule object.
|
|
133
|
+
"""
|
|
134
|
+
rule_string = yara_rule_string(pattern, pattern_type, rule_name, pattern_label, modifier)
|
|
135
|
+
return yara.compile(source=rule_string)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def safe_label(_label: str) -> str:
|
|
139
|
+
"""
|
|
140
|
+
YARA rule and pattern names can only contain alphanumeric chars.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
_label (str): The label to sanitize.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
str: A sanitized label safe for use in YARA rules.
|
|
147
|
+
"""
|
|
148
|
+
label = _label
|
|
149
|
+
|
|
150
|
+
for char, replacement in SAFE_LABEL_REPLACEMENTS.items():
|
|
151
|
+
if replacement != UNDERSCORE:
|
|
152
|
+
label = label.replace(char, f"__{replacement.upper()}__")
|
|
153
|
+
else:
|
|
154
|
+
label = label.replace(char, replacement)
|
|
155
|
+
|
|
156
|
+
if re.match('^\\d', label):
|
|
157
|
+
label = '_' + label
|
|
158
|
+
|
|
159
|
+
if not re.match('\\w+', label):
|
|
160
|
+
msg = f"'{label}' is invalid: YARA labels must be alphanumeric/underscore and cannot start with a number"
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
|
|
163
|
+
log.debug(f"Built safe label {label} from {_label}")
|
|
164
|
+
return label
|
yaralyzer/yaralyzer.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Main Yaralyzer class and alternate constructors."""
|
|
2
|
+
from os import path
|
|
3
|
+
from typing import Callable, Iterator, List, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import yara
|
|
6
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
7
|
+
from rich.padding import Padding
|
|
8
|
+
from rich.style import Style
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from yaralyzer.bytes_match import BytesMatch
|
|
12
|
+
from yaralyzer.config import YARALYZE, YaralyzerConfig
|
|
13
|
+
from yaralyzer.decoding.bytes_decoder import BytesDecoder
|
|
14
|
+
from yaralyzer.helpers.file_helper import files_in_dir, load_binary_data
|
|
15
|
+
from yaralyzer.helpers.rich_text_helper import dim_if, reverse_color, print_fatal_error_and_exit
|
|
16
|
+
from yaralyzer.helpers.string_helper import comma_join, newline_join
|
|
17
|
+
from yaralyzer.output.file_hashes_table import bytes_hashes_table
|
|
18
|
+
from yaralyzer.output.regex_match_metrics import RegexMatchMetrics
|
|
19
|
+
from yaralyzer.output.rich_console import YARALYZER_THEME, console
|
|
20
|
+
from yaralyzer.util.logging import log
|
|
21
|
+
from yaralyzer.yara.yara_match import YaraMatch
|
|
22
|
+
from yaralyzer.yara.yara_rule_builder import yara_rule_string
|
|
23
|
+
|
|
24
|
+
YARA_FILE_DOES_NOT_EXIST_ERROR_MSG = "is not a valid yara rules file (it doesn't exist)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# TODO: might be worth introducing a Scannable namedtuple or similar
|
|
28
|
+
class Yaralyzer:
|
|
29
|
+
"""
|
|
30
|
+
Central class that handles setting up / compiling YARA rules and reading binary data from files as needed.
|
|
31
|
+
|
|
32
|
+
Alternate constructors are provided depending on whether:
|
|
33
|
+
|
|
34
|
+
* YARA rules are already compiled
|
|
35
|
+
|
|
36
|
+
* YARA rules should be compiled from a string
|
|
37
|
+
|
|
38
|
+
* YARA rules should be read from a file
|
|
39
|
+
|
|
40
|
+
* YARA rules should be read from a directory of .yara files
|
|
41
|
+
|
|
42
|
+
The real action happens in the `__rich__console__()` dunder method.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
bytes (bytes): The binary data to scan.
|
|
46
|
+
bytes_length (int): The length of the binary data.
|
|
47
|
+
scannable_label (str): A label for the binary data, typically the filename or a user-provided label.
|
|
48
|
+
rules (yara.Rules): The compiled YARA rules to use for scanning.
|
|
49
|
+
rules_label (str): A label for the ruleset, typically derived from filenames or user input.
|
|
50
|
+
highlight_style (str): The style to use for highlighting matches in the output.
|
|
51
|
+
non_matches (List[dict]): A list of YARA rules that did not match the binary data.
|
|
52
|
+
matches (List[YaraMatch]): A list of YaraMatch objects representing the matches found.
|
|
53
|
+
extraction_stats (RegexMatchMetrics): Metrics related to decoding attempts on matched data
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
rules: Union[str, yara.Rules],
|
|
59
|
+
rules_label: str,
|
|
60
|
+
scannable: Union[bytes, str],
|
|
61
|
+
scannable_label: Optional[str] = None,
|
|
62
|
+
highlight_style: str = YaralyzerConfig.HIGHLIGHT_STYLE
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Initialize a `Yaralyzer` instance for scanning binary data with YARA rules.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
rules (Union[str, yara.Rules]): YARA rules to use for scanning. Can be a string or a pre-compiled
|
|
69
|
+
`yara.Rules` object (strings will be compiled to an instance of `yara.Rules`).
|
|
70
|
+
rules_label (str): Label to identify the ruleset in output and logs.
|
|
71
|
+
scannable (Union[bytes, str]): The data to scan. If it's `bytes` type then that data is scanned;
|
|
72
|
+
if it's a string it is treated as a file path to load bytes from.
|
|
73
|
+
scannable_label (Optional[str], optional): Label for the `scannable` arg data.
|
|
74
|
+
Required if `scannable` is `bytes`.
|
|
75
|
+
If `scannable` is a file path `scannable_label` will default to the file's basename.
|
|
76
|
+
highlight_style (str, optional): Style to use for highlighting matches in output.
|
|
77
|
+
Defaults to `YaralyzerConfig.HIGHLIGHT_STYLE`.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
TypeError: If `scannable` is `bytes` and `scannable_label` is not provided.
|
|
81
|
+
"""
|
|
82
|
+
if 'args' not in vars(YaralyzerConfig):
|
|
83
|
+
YaralyzerConfig.set_default_args()
|
|
84
|
+
|
|
85
|
+
yara.set_config(
|
|
86
|
+
stack_size=YaralyzerConfig.args.yara_stack_size,
|
|
87
|
+
max_match_data=YaralyzerConfig.args.max_match_length
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if isinstance(scannable, bytes):
|
|
91
|
+
if scannable_label is None:
|
|
92
|
+
raise TypeError("Must provide 'scannable_label' arg when yaralyzing raw bytes")
|
|
93
|
+
|
|
94
|
+
self.bytes: bytes = scannable
|
|
95
|
+
self.scannable_label: str = scannable_label
|
|
96
|
+
else:
|
|
97
|
+
self.bytes: bytes = load_binary_data(scannable)
|
|
98
|
+
self.scannable_label: str = scannable_label or path.basename(scannable)
|
|
99
|
+
|
|
100
|
+
if isinstance(rules, yara.Rules):
|
|
101
|
+
self.rules: yara.Rules = rules
|
|
102
|
+
else:
|
|
103
|
+
log.info(f"Compiling YARA rules from provided string:\n{rules}")
|
|
104
|
+
self.rules: yara.Rules = yara.compile(source=rules)
|
|
105
|
+
|
|
106
|
+
self.bytes_length: int = len(self.bytes)
|
|
107
|
+
self.rules_label: str = rules_label
|
|
108
|
+
self.highlight_style: str = highlight_style
|
|
109
|
+
# Outcome tracking variables
|
|
110
|
+
self.non_matches: List[dict] = []
|
|
111
|
+
self.matches: List[YaraMatch] = []
|
|
112
|
+
self.extraction_stats = RegexMatchMetrics()
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def for_rules_files(
|
|
116
|
+
cls,
|
|
117
|
+
yara_rules_files: List[str],
|
|
118
|
+
scannable: Union[bytes, str],
|
|
119
|
+
scannable_label: Optional[str] = None
|
|
120
|
+
) -> 'Yaralyzer':
|
|
121
|
+
"""
|
|
122
|
+
Alternate constructor to load YARA rules from files and label rules with the filenames.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
yara_rules_files (List[str]): List of file paths to YARA rules files.
|
|
126
|
+
scannable (Union[bytes, str]): The data to scan. If `bytes`, raw data is scanned;
|
|
127
|
+
if `str`, it is treated as a file path to load bytes from.
|
|
128
|
+
scannable_label (Optional[str], optional): Label for the `scannable` data.
|
|
129
|
+
Required if `scannable` is `bytes`. If scannable is a file path, defaults to the file's basename.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
FileNotFoundError: If any file in `yara_rules_files` does not exist.
|
|
133
|
+
TypeError: If `yara_rules_files` is not a list.
|
|
134
|
+
"""
|
|
135
|
+
if not isinstance(yara_rules_files, list):
|
|
136
|
+
raise TypeError(f"{yara_rules_files} is not a list")
|
|
137
|
+
|
|
138
|
+
for file in yara_rules_files:
|
|
139
|
+
if not path.exists(file):
|
|
140
|
+
raise FileNotFoundError(f"'{file}' {YARA_FILE_DOES_NOT_EXIST_ERROR_MSG}")
|
|
141
|
+
|
|
142
|
+
filepaths_arg = {path.basename(file): file for file in yara_rules_files}
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
yara_rules = yara.compile(filepaths=filepaths_arg)
|
|
146
|
+
except yara.SyntaxError as e:
|
|
147
|
+
print_fatal_error_and_exit(f"Failed to parse YARA rules file(s): {e}")
|
|
148
|
+
|
|
149
|
+
yara_rules_label = comma_join(yara_rules_files, func=path.basename)
|
|
150
|
+
return cls(yara_rules, yara_rules_label, scannable, scannable_label)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def for_rules_dirs(
|
|
154
|
+
cls,
|
|
155
|
+
dirs: List[str],
|
|
156
|
+
scannable: Union[bytes, str],
|
|
157
|
+
scannable_label: Optional[str] = None
|
|
158
|
+
) -> 'Yaralyzer':
|
|
159
|
+
"""
|
|
160
|
+
Alternate constructor that will load all `.yara` files in `yara_rules_dir`.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
dirs (List[str]): List of directories to search for `.yara` files.
|
|
164
|
+
scannable (Union[bytes, str]): The data to scan. If `bytes`, raw data is scanned;
|
|
165
|
+
if `str`, it is treated as a file path to load bytes from.
|
|
166
|
+
scannable_label (Optional[str], optional): Label for the `scannable` data.
|
|
167
|
+
Required if `scannable` is `bytes`. If scannable is a file path, defaults to the file's basename.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
FileNotFoundError: If `dirs` is not a list of valid directories.
|
|
171
|
+
"""
|
|
172
|
+
if not (isinstance(dirs, list) and all(path.isdir(dir) for dir in dirs)):
|
|
173
|
+
raise FileNotFoundError(f"'{dirs}' is not a list of valid directories")
|
|
174
|
+
|
|
175
|
+
rules_files = [path.join(dir, f) for dir in dirs for f in files_in_dir(dir)]
|
|
176
|
+
return cls.for_rules_files(rules_files, scannable, scannable_label)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def for_patterns(
|
|
180
|
+
cls,
|
|
181
|
+
patterns: List[str],
|
|
182
|
+
patterns_type: str,
|
|
183
|
+
scannable: Union[bytes, str],
|
|
184
|
+
scannable_label: Optional[str] = None,
|
|
185
|
+
rules_label: Optional[str] = None,
|
|
186
|
+
pattern_label: Optional[str] = None,
|
|
187
|
+
regex_modifier: Optional[str] = None,
|
|
188
|
+
) -> 'Yaralyzer':
|
|
189
|
+
"""
|
|
190
|
+
Alternate constructor taking regex pattern strings. Rules label defaults to the patterns joined by comma.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
patterns (List[str]): List of regex or hex patterns to build rules from.
|
|
194
|
+
patterns_type (str): Either `"regex"` or `"hex"` to indicate the type of patterns provided.
|
|
195
|
+
scannable (Union[bytes, str]): The data to scan. If `bytes`, raw data is scanned;
|
|
196
|
+
if `str`, it is treated as a file path to load bytes from.
|
|
197
|
+
scannable_label (Optional[str], optional): Label for the `scannable` data.
|
|
198
|
+
Required if `scannable` is `bytes`.
|
|
199
|
+
If scannable is a file path, defaults to the file's basename.
|
|
200
|
+
rules_label (Optional[str], optional): Label for the ruleset. Defaults to the patterns joined by comma.
|
|
201
|
+
pattern_label (Optional[str], optional): Label for each pattern in the YARA rules. Defaults to "pattern".
|
|
202
|
+
regex_modifier (Optional[str], optional): Optional regex modifier (e.g. "nocase", "ascii", "wide", etc).
|
|
203
|
+
Only valid if `patterns_type` is `"regex"`.
|
|
204
|
+
"""
|
|
205
|
+
rule_strings = []
|
|
206
|
+
|
|
207
|
+
for i, pattern in enumerate(patterns):
|
|
208
|
+
suffix = f"_{i + 1}" if len(patterns) > 1 else ''
|
|
209
|
+
|
|
210
|
+
rule_strings.append(yara_rule_string(
|
|
211
|
+
pattern=pattern,
|
|
212
|
+
pattern_type=patterns_type,
|
|
213
|
+
rule_name=f"{rules_label or YARALYZE}{suffix}",
|
|
214
|
+
pattern_label=f"{pattern_label}{suffix}" if pattern_label else None,
|
|
215
|
+
modifier=regex_modifier
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
rules_string = newline_join(rule_strings)
|
|
219
|
+
rules_label = comma_join(patterns)
|
|
220
|
+
return cls(rules_string, rules_label, scannable, scannable_label)
|
|
221
|
+
|
|
222
|
+
def yaralyze(self) -> None:
|
|
223
|
+
"""Use YARA to find matches and then force decode them."""
|
|
224
|
+
console.print(self)
|
|
225
|
+
|
|
226
|
+
def match_iterator(self) -> Iterator[Tuple[BytesMatch, BytesDecoder]]:
|
|
227
|
+
"""
|
|
228
|
+
Iterator version of `yaralyze()`.
|
|
229
|
+
|
|
230
|
+
Yields:
|
|
231
|
+
Tuple[BytesMatch, BytesDecoder]: Match and decode data tuple.
|
|
232
|
+
"""
|
|
233
|
+
self.rules.match(data=self.bytes, callback=self._yara_callback)
|
|
234
|
+
|
|
235
|
+
for yara_match in self.matches:
|
|
236
|
+
console.print(yara_match)
|
|
237
|
+
console.line()
|
|
238
|
+
|
|
239
|
+
for match in BytesMatch.from_yara_match(self.bytes, yara_match.match, self.highlight_style):
|
|
240
|
+
decoder = BytesDecoder(match, yara_match.rule_name)
|
|
241
|
+
self.extraction_stats.tally_match(decoder)
|
|
242
|
+
yield match, decoder
|
|
243
|
+
|
|
244
|
+
self._print_non_matches()
|
|
245
|
+
|
|
246
|
+
def _yara_callback(self, data: dict) -> Callable:
|
|
247
|
+
"""
|
|
248
|
+
Callback invoked by `yara-python` to handle matches and non-matches as they are discovered.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
data (dict): Data provided when `yara-python` invokes the callback.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Callable: Always returns `yara.CALLBACK_CONTINUE` to signal `yara-python` should continue processing.
|
|
255
|
+
"""
|
|
256
|
+
if data['matches']:
|
|
257
|
+
self.matches.append(YaraMatch(data, self._panel_text()))
|
|
258
|
+
else:
|
|
259
|
+
self.non_matches.append(data)
|
|
260
|
+
|
|
261
|
+
return yara.CALLBACK_CONTINUE
|
|
262
|
+
|
|
263
|
+
def _print_non_matches(self) -> None:
|
|
264
|
+
"""Print info about the YARA rules that didn't match the bytes."""
|
|
265
|
+
if len(self.non_matches) == 0:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
non_matches_text = sorted([Text(nm['rule'], 'grey') for nm in self.non_matches], key=str)
|
|
269
|
+
|
|
270
|
+
# Only show the non matches if there were valid ones, otherwise just show the number
|
|
271
|
+
if len(self.matches) == 0:
|
|
272
|
+
non_match_desc = f" did not match any of the {len(self.non_matches)} yara rules"
|
|
273
|
+
console.print(dim_if(self.__text__() + Text(non_match_desc, style='grey'), True))
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
non_match_desc = f" did not match the other {len(self.non_matches)} yara rules"
|
|
277
|
+
console.print(self.__text__() + Text(non_match_desc, style='grey') + Text(': '), style='dim')
|
|
278
|
+
console.print(Padding(Text(', ', 'white').join(non_matches_text), (0, 0, 1, 4)))
|
|
279
|
+
|
|
280
|
+
def _panel_text(self) -> Text:
|
|
281
|
+
"""Inverted colors for the panel at the top of the match section of the output."""
|
|
282
|
+
styles = [reverse_color(YARALYZER_THEME.styles[f"yara.{s}"]) for s in ('scanned', 'rules')]
|
|
283
|
+
return self.__text__(*styles)
|
|
284
|
+
|
|
285
|
+
def _filename_string(self) -> str:
|
|
286
|
+
"""The string to use when exporting this yaralyzer to SVG/HTML/etc."""
|
|
287
|
+
return str(self).replace('>', '').replace('<', '').replace(' ', '_')
|
|
288
|
+
|
|
289
|
+
def __text__(self, byte_style: Style | str = 'yara.scanned', rule_style: Style | str = 'yara.rules') -> Text:
|
|
290
|
+
"""Text representation of this YARA scan (__text__() was taken)."""
|
|
291
|
+
txt = Text('').append(self.scannable_label, style=byte_style or 'yara.scanned')
|
|
292
|
+
return txt.append(' scanned with <').append(self.rules_label, style=rule_style or 'yara.rules').append('>')
|
|
293
|
+
|
|
294
|
+
def __rich_console__(self, _console: Console, options: ConsoleOptions) -> RenderResult:
|
|
295
|
+
"""Does the stuff. TODO: not the best place to put the core logic."""
|
|
296
|
+
yield bytes_hashes_table(self.bytes, self.scannable_label)
|
|
297
|
+
|
|
298
|
+
for _bytes_match, bytes_decoder in self.match_iterator():
|
|
299
|
+
for attempt in bytes_decoder.__rich_console__(_console, options):
|
|
300
|
+
yield attempt
|
|
301
|
+
|
|
302
|
+
def __str__(self) -> str:
|
|
303
|
+
"""Plain text (no rich colors) representation of the scan for display."""
|
|
304
|
+
return self.__text__().plain
|