yaralyzer 1.0.7__tar.gz → 1.0.8__tar.gz
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.
Potentially problematic release.
This version of yaralyzer might be problematic. Click here for more details.
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/CHANGELOG.md +6 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/PKG-INFO +8 -6
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/README.md +3 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/pyproject.toml +33 -15
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/__init__.py +5 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/bytes_match.py +109 -18
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/config.py +17 -5
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/decoding/bytes_decoder.py +31 -9
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/decoding/decoding_attempt.py +7 -7
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/encoding_detection/character_encodings.py +2 -1
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/encoding_detection/encoding_assessment.py +8 -2
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/encoding_detection/encoding_detector.py +14 -9
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/bytes_helper.py +112 -15
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/dict_helper.py +1 -1
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/file_helper.py +3 -3
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/rich_text_helper.py +6 -4
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/string_helper.py +1 -1
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/output/file_export.py +1 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/output/file_hashes_table.py +30 -2
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/output/regex_match_metrics.py +13 -10
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/output/rich_console.py +17 -2
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/util/argument_parser.py +1 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/util/logging.py +5 -5
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/yaralyzer.py +39 -23
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/.yaralyzer.example +0 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/LICENSE +0 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/helpers/list_helper.py +0 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/output/decoding_attempts_table.py +0 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/yara/yara_match.py +0 -0
- {yaralyzer-1.0.7 → yaralyzer-1.0.8}/yaralyzer/yara/yara_rule_builder.py +0 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# NEXT RELEASE
|
|
2
2
|
|
|
3
|
+
### 1.0.8
|
|
4
|
+
* Bump `python-dotenv` to v1.1.1
|
|
5
|
+
* Use `mkdocs` and `lazydocs` to build automatic API documentation at https://michelcrypt4d4mus.github.io/yaralyzer/
|
|
6
|
+
* Drop python 3.9 support (required by `mkdocs-awesome-nav` package)
|
|
7
|
+
|
|
3
8
|
### 1.0.7
|
|
4
9
|
* Add `Changelog` to PyPi URLs, add some more PyPi classifiers
|
|
5
10
|
* Add `.flake8` config file and fix style errors
|
|
11
|
+
* Rename `prefix_with_plain_text_obj()` to `prefix_with_style()`
|
|
6
12
|
|
|
7
13
|
### 1.0.6
|
|
8
14
|
* Add `Environment :: Console` and `Programming Language :: Python` to PyPi classifiers
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: yaralyzer
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: Visualize and force decode YARA and regex matches found in a file or byte stream
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: Visualize and force decode YARA and regex matches found in a file or byte stream with colors. Lots of colors.
|
|
5
5
|
Home-page: https://github.com/michelcrypt4d4mus/yaralyzer
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
7
|
Keywords: ascii art,binary,character encoding,color,cybersecurity,data visualization,decode,DFIR,encoding,infosec,maldoc,malicious,malware,malware analysis,regex,regular expressions,reverse engineering,reversing,security,threat assessment,threat hunting,threat intelligence,threat research,threatintel,visualization,yara
|
|
8
8
|
Author: Michel de Cryptadamus
|
|
9
9
|
Author-email: michel@cryptadamus.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Intended Audience :: Information Technology
|
|
14
14
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -23,12 +22,12 @@ Classifier: Topic :: Artistic Software
|
|
|
23
22
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
24
23
|
Classifier: Topic :: Security
|
|
25
24
|
Requires-Dist: chardet (>=5.0.0,<6.0.0)
|
|
26
|
-
Requires-Dist: python-dotenv (>=
|
|
25
|
+
Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
|
|
27
26
|
Requires-Dist: rich (>=14.1.0,<15.0.0)
|
|
28
27
|
Requires-Dist: rich-argparse-plus (>=0.3.1,<0.4.0)
|
|
29
28
|
Requires-Dist: yara-python (>=4.5.4,<5.0.0)
|
|
30
29
|
Project-URL: Changelog, https://github.com/michelcrypt4d4mus/yaralyzer/blob/master/CHANGELOG.md
|
|
31
|
-
Project-URL: Documentation, https://github.
|
|
30
|
+
Project-URL: Documentation, https://michelcrypt4d4mus.github.io/yaralyzer/
|
|
32
31
|
Project-URL: Repository, https://github.com/michelcrypt4d4mus/yaralyzer
|
|
33
32
|
Description-Content-Type: text/markdown
|
|
34
33
|
|
|
@@ -120,6 +119,9 @@ for bytes_match, bytes_decoder in yaralyzer.match_iterator():
|
|
|
120
119
|
do_stuff()
|
|
121
120
|
```
|
|
122
121
|
|
|
122
|
+
#### API Documentation
|
|
123
|
+
Auto generated documentation for Yaralyzer's various classes and methods can be found [here](https://michelcrypt4d4mus.github.io/yaralyzer/).
|
|
124
|
+
|
|
123
125
|
# Example Output
|
|
124
126
|
The Yaralyzer can export visualizations to HTML, ANSI colored text, and SVG vector images using the file export functionality that comes with [Rich](https://github.com/Textualize/rich) as well as a (somewhat limited) plain text JSON format. SVGs can be turned into `png` format images with a tool like [Inkscape](https://inkscape.org/) or `cairosvg`. In our experience they both work though we've seen some glitchiness with `cairosvg`.
|
|
125
127
|
|
|
@@ -86,6 +86,9 @@ for bytes_match, bytes_decoder in yaralyzer.match_iterator():
|
|
|
86
86
|
do_stuff()
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
#### API Documentation
|
|
90
|
+
Auto generated documentation for Yaralyzer's various classes and methods can be found [here](https://michelcrypt4d4mus.github.io/yaralyzer/).
|
|
91
|
+
|
|
89
92
|
# Example Output
|
|
90
93
|
The Yaralyzer can export visualizations to HTML, ANSI colored text, and SVG vector images using the file export functionality that comes with [Rich](https://github.com/Textualize/rich) as well as a (somewhat limited) plain text JSON format. SVGs can be turned into `png` format images with a tool like [Inkscape](https://inkscape.org/) or `cairosvg`. In our experience they both work though we've seen some glitchiness with `cairosvg`.
|
|
91
94
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "yaralyzer"
|
|
3
|
-
version = "1.0.
|
|
4
|
-
description = "Visualize and force decode YARA and regex matches found in a file or byte stream
|
|
3
|
+
version = "1.0.8"
|
|
4
|
+
description = "Visualize and force decode YARA and regex matches found in a file or byte stream with colors. Lots of colors."
|
|
5
5
|
authors = ["Michel de Cryptadamus <michel@cryptadamus.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "GPL-3.0-or-later"
|
|
8
|
+
|
|
8
9
|
homepage = "https://github.com/michelcrypt4d4mus/yaralyzer"
|
|
9
10
|
repository = "https://github.com/michelcrypt4d4mus/yaralyzer"
|
|
10
|
-
documentation = "https://github.
|
|
11
|
+
documentation = "https://michelcrypt4d4mus.github.io/yaralyzer/"
|
|
11
12
|
|
|
12
13
|
classifiers = [
|
|
13
14
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -15,7 +16,6 @@ classifiers = [
|
|
|
15
16
|
"Intended Audience :: Information Technology",
|
|
16
17
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
17
18
|
"Programming Language :: Python",
|
|
18
|
-
"Programming Language :: Python :: 3.9",
|
|
19
19
|
"Programming Language :: Python :: 3.10",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
@@ -61,13 +61,13 @@ keywords = [
|
|
|
61
61
|
]
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
# Dependencies
|
|
66
|
-
|
|
64
|
+
####################
|
|
65
|
+
# Dependencies #
|
|
66
|
+
####################
|
|
67
67
|
[tool.poetry.dependencies]
|
|
68
|
-
python = "^3.
|
|
68
|
+
python = "^3.10"
|
|
69
69
|
chardet = ">=5.0.0,<6.0.0"
|
|
70
|
-
python-dotenv = "^
|
|
70
|
+
python-dotenv = "^1.1.1"
|
|
71
71
|
rich = "^14.1.0"
|
|
72
72
|
rich-argparse-plus = "^0.3.1"
|
|
73
73
|
yara-python = "^4.5.4"
|
|
@@ -75,6 +75,12 @@ yara-python = "^4.5.4"
|
|
|
75
75
|
|
|
76
76
|
[tool.poetry.group.dev.dependencies]
|
|
77
77
|
flake8 = "^7.3.0"
|
|
78
|
+
lazydocs = "^0.4.8"
|
|
79
|
+
mkdocs = "^1.6.1"
|
|
80
|
+
mkdocs-awesome-nav = "^3.1.2"
|
|
81
|
+
mkdocs-include-markdown-plugin = "^7.1.7"
|
|
82
|
+
mkdocs-material = "^9.6.19"
|
|
83
|
+
pydocstyle = "^6.3.0"
|
|
78
84
|
pytest = "^7.1.3"
|
|
79
85
|
|
|
80
86
|
|
|
@@ -86,16 +92,28 @@ yaralyze = 'yaralyzer:yaralyze'
|
|
|
86
92
|
yaralyzer_show_color_theme = 'yaralyzer.helpers.rich_text_helper:yaralyzer_show_color_theme'
|
|
87
93
|
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
|
|
95
|
+
###############
|
|
96
|
+
# PyPi URLs #
|
|
97
|
+
###############
|
|
92
98
|
[tool.poetry.urls]
|
|
93
99
|
Changelog = "https://github.com/michelcrypt4d4mus/yaralyzer/blob/master/CHANGELOG.md"
|
|
94
100
|
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
|
|
102
|
+
#################
|
|
103
|
+
# Build Stuff #
|
|
104
|
+
#################
|
|
99
105
|
[build-system]
|
|
100
106
|
build-backend = "poetry.core.masonry.api"
|
|
101
107
|
requires = ["poetry-core"]
|
|
108
|
+
|
|
109
|
+
[tool.pydocstyle]
|
|
110
|
+
match-dir = "yaralyzer"
|
|
111
|
+
ignore = [
|
|
112
|
+
"D200", # One-line docstring should fit on one line with quotes (found 3)
|
|
113
|
+
"D203", # 1 blank line required before class docstring"
|
|
114
|
+
"D212", # Multi-line docstring summary should start at the first line
|
|
115
|
+
"D401", # First line should be in imperative mood"
|
|
116
|
+
"D406", # Section name should end with a newline
|
|
117
|
+
"D407", # Missing dashed underline after section
|
|
118
|
+
"D413", # Missing blank line after last section
|
|
119
|
+
]
|
|
@@ -24,6 +24,11 @@ PDFALYZER_MSG_TXT.append('https://github.com/michelcrypt4d4mus/pdfalyzer\n', sty
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def yaralyze():
|
|
27
|
+
"""
|
|
28
|
+
Entry point for yaralyzer when invoked as a script.
|
|
29
|
+
|
|
30
|
+
Args are parsed from the command line and environment variables. See yaralyzer --help for details.
|
|
31
|
+
"""
|
|
27
32
|
args = parse_arguments()
|
|
28
33
|
output_basepath = None
|
|
29
34
|
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Simple class to keep track of regex matches against binary data. Basically an re.match object with
|
|
3
|
-
some (not many) extra bells and whistles, most notably the surrounding_bytes property.
|
|
4
|
-
|
|
5
|
-
pre_capture_len and post_capture_len refer to the regex sections before and after the capture group,
|
|
6
|
-
e.g. a regex like '123(.*)x:' would have pre_capture_len of 3 and post_capture_len of 2.
|
|
7
|
-
"""
|
|
1
|
+
"""BytesMatch class for tracking regex and YARA matches against binary data."""
|
|
8
2
|
import re
|
|
9
3
|
from typing import Iterator, Optional
|
|
10
4
|
|
|
@@ -19,6 +13,16 @@ from yaralyzer.output.rich_console import ALERT_STYLE, GREY_ADDRESS
|
|
|
19
13
|
|
|
20
14
|
|
|
21
15
|
class BytesMatch:
|
|
16
|
+
"""
|
|
17
|
+
Simple class to keep track of regex matches against binary data.
|
|
18
|
+
|
|
19
|
+
Basically an re.match object with some (not many) extra bells and whistles, most notably
|
|
20
|
+
the surrounding_bytes property.
|
|
21
|
+
|
|
22
|
+
pre_capture_len and post_capture_len refer to the regex sections before and after the capture group,
|
|
23
|
+
e.g. a regex like '123(.*)x:' would have pre_capture_len of 3 and post_capture_len of 2.
|
|
24
|
+
"""
|
|
25
|
+
|
|
22
26
|
def __init__(
|
|
23
27
|
self,
|
|
24
28
|
matched_against: bytes,
|
|
@@ -30,8 +34,16 @@ class BytesMatch:
|
|
|
30
34
|
highlight_style: str = YaralyzerConfig.HIGHLIGHT_STYLE
|
|
31
35
|
) -> None:
|
|
32
36
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
Initialize a BytesMatch object representing a match against binary data.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
matched_against (bytes): The full byte sequence that was searched.
|
|
41
|
+
start_idx (int): Start index of the match in the byte sequence.
|
|
42
|
+
length (int): Length of the match in bytes.
|
|
43
|
+
label (str): Label for the match (e.g., regex or YARA rule name).
|
|
44
|
+
ordinal (int): The Nth match for this pattern.
|
|
45
|
+
match (Optional[re.Match]): Regex match object, if available.
|
|
46
|
+
highlight_style (str): Style to use for highlighting the match.
|
|
35
47
|
"""
|
|
36
48
|
self.matched_against: bytes = matched_against
|
|
37
49
|
self.start_idx: int = start_idx
|
|
@@ -58,6 +70,18 @@ class BytesMatch:
|
|
|
58
70
|
ordinal: int,
|
|
59
71
|
highlight_style: str = YaralyzerConfig.HIGHLIGHT_STYLE
|
|
60
72
|
) -> 'BytesMatch':
|
|
73
|
+
"""
|
|
74
|
+
Create a BytesMatch from a regex match object.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
matched_against (bytes): The bytes searched.
|
|
78
|
+
match (re.Match): The regex match object.
|
|
79
|
+
ordinal (int): The Nth match for this pattern.
|
|
80
|
+
highlight_style (str): Style for highlighting.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
BytesMatch: The constructed BytesMatch instance.
|
|
84
|
+
"""
|
|
61
85
|
return cls(matched_against, match.start(), len(match[0]), match.re.pattern, ordinal, match, highlight_style)
|
|
62
86
|
|
|
63
87
|
@classmethod
|
|
@@ -70,7 +94,20 @@ class BytesMatch:
|
|
|
70
94
|
ordinal: int,
|
|
71
95
|
highlight_style: str = YaralyzerConfig.HIGHLIGHT_STYLE
|
|
72
96
|
) -> 'BytesMatch':
|
|
73
|
-
"""
|
|
97
|
+
"""
|
|
98
|
+
Build a BytesMatch from a YARA string match instance.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
matched_against (bytes): The bytes searched.
|
|
102
|
+
rule_name (str): Name of the YARA rule.
|
|
103
|
+
yara_str_match (StringMatch): YARA string match object.
|
|
104
|
+
yara_str_match_instance (StringMatchInstance): Instance of the string match.
|
|
105
|
+
ordinal (int): The Nth match for this pattern.
|
|
106
|
+
highlight_style (str): Style for highlighting.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
BytesMatch: The constructed BytesMatch instance.
|
|
110
|
+
"""
|
|
74
111
|
pattern_label = yara_str_match.identifier
|
|
75
112
|
|
|
76
113
|
# Don't duplicate the labeling if rule_name and yara_str are the same
|
|
@@ -94,7 +131,17 @@ class BytesMatch:
|
|
|
94
131
|
yara_match: dict,
|
|
95
132
|
highlight_style: str = YaralyzerConfig.HIGHLIGHT_STYLE
|
|
96
133
|
) -> Iterator['BytesMatch']:
|
|
97
|
-
"""
|
|
134
|
+
"""
|
|
135
|
+
Yield a BytesMatch for each string returned as part of a YARA match result dict.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
matched_against (bytes): The bytes searched.
|
|
139
|
+
yara_match (dict): YARA match result dictionary.
|
|
140
|
+
highlight_style (str): Style for highlighting.
|
|
141
|
+
|
|
142
|
+
Yields:
|
|
143
|
+
BytesMatch: For each string match in the YARA result.
|
|
144
|
+
"""
|
|
98
145
|
i = 0 # For numbered labeling
|
|
99
146
|
|
|
100
147
|
# yara-python's internals changed with 4.3.0: https://github.com/VirusTotal/yara-python/releases/tag/v4.3.0
|
|
@@ -112,14 +159,27 @@ class BytesMatch:
|
|
|
112
159
|
)
|
|
113
160
|
|
|
114
161
|
def style_at_position(self, idx) -> str:
|
|
115
|
-
"""
|
|
162
|
+
"""
|
|
163
|
+
Get the style for the byte at position idx within the matched bytes.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
idx (int): Index within the surrounding bytes.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
str: The style to use for this byte (highlight or greyed out).
|
|
170
|
+
"""
|
|
116
171
|
if idx < self.highlight_start_idx or idx >= self.highlight_end_idx:
|
|
117
172
|
return GREY_ADDRESS
|
|
118
173
|
else:
|
|
119
174
|
return self.highlight_style
|
|
120
175
|
|
|
121
176
|
def location(self) -> Text:
|
|
122
|
-
"""
|
|
177
|
+
"""
|
|
178
|
+
Get a styled Text object describing the start and end index of the match.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Text: Rich Text object like '(start idx: 348190, end idx: 348228)'.
|
|
182
|
+
"""
|
|
123
183
|
location_txt = prefix_with_style(
|
|
124
184
|
f"(start idx: ",
|
|
125
185
|
style='off_white',
|
|
@@ -133,13 +193,26 @@ class BytesMatch:
|
|
|
133
193
|
return location_txt
|
|
134
194
|
|
|
135
195
|
def is_decodable(self) -> bool:
|
|
136
|
-
"""
|
|
196
|
+
"""
|
|
197
|
+
Determine if the matched bytes should be decoded.
|
|
198
|
+
|
|
199
|
+
Whether the bytes are decodable depends on whether SUPPRESS_DECODES_TABLE is set
|
|
200
|
+
and whether the match length is between MIN/MAX_DECODE_LENGTH.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
bool: True if decodable, False otherwise.
|
|
204
|
+
"""
|
|
137
205
|
return self.match_length >= YaralyzerConfig.args.min_decode_length \
|
|
138
206
|
and self.match_length <= YaralyzerConfig.args.max_decode_length \
|
|
139
207
|
and not YaralyzerConfig.args.suppress_decodes_table
|
|
140
208
|
|
|
141
209
|
def bytes_hashes_table(self) -> Table:
|
|
142
|
-
"""
|
|
210
|
+
"""
|
|
211
|
+
Build a table of MD5/SHA hashes for the matched bytes.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Table: Rich Table object with hashes.
|
|
215
|
+
"""
|
|
143
216
|
return bytes_hashes_table(
|
|
144
217
|
self.bytes,
|
|
145
218
|
self.location().plain,
|
|
@@ -147,7 +220,12 @@ class BytesMatch:
|
|
|
147
220
|
)
|
|
148
221
|
|
|
149
222
|
def suppression_notice(self) -> Text:
|
|
150
|
-
"""
|
|
223
|
+
"""
|
|
224
|
+
Generate a message for when the match is too short or too long to decode.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Text: Rich Text object with the suppression notice.
|
|
228
|
+
"""
|
|
151
229
|
txt = self.__rich__()
|
|
152
230
|
|
|
153
231
|
if self.match_length < YaralyzerConfig.args.min_decode_length:
|
|
@@ -159,7 +237,12 @@ class BytesMatch:
|
|
|
159
237
|
return txt
|
|
160
238
|
|
|
161
239
|
def to_json(self) -> dict:
|
|
162
|
-
"""
|
|
240
|
+
"""
|
|
241
|
+
Convert this BytesMatch to a JSON-serializable dictionary.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
dict: Dictionary representation of the match, suitable for JSON serialization.
|
|
245
|
+
"""
|
|
163
246
|
json_dict = {
|
|
164
247
|
'label': self.label,
|
|
165
248
|
'match_length': self.match_length,
|
|
@@ -178,7 +261,13 @@ class BytesMatch:
|
|
|
178
261
|
return json_dict
|
|
179
262
|
|
|
180
263
|
def _find_surrounding_bytes(self, num_before: Optional[int] = None, num_after: Optional[int] = None) -> None:
|
|
181
|
-
"""
|
|
264
|
+
"""
|
|
265
|
+
Find and set the bytes surrounding the match, ensuring indices stay within bounds.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
num_before (Optional[int]): Number of bytes before the match to include.
|
|
269
|
+
num_after (Optional[int]): Number of bytes after the match to include.
|
|
270
|
+
"""
|
|
182
271
|
num_after = num_after or num_before or YaralyzerConfig.args.surrounding_bytes
|
|
183
272
|
num_before = num_before or YaralyzerConfig.args.surrounding_bytes
|
|
184
273
|
self.surrounding_start_idx: int = max(self.start_idx - num_before, 0)
|
|
@@ -186,6 +275,7 @@ class BytesMatch:
|
|
|
186
275
|
self.surrounding_bytes: bytes = self.matched_against[self.surrounding_start_idx:self.surrounding_end_idx]
|
|
187
276
|
|
|
188
277
|
def __rich__(self) -> Text:
|
|
278
|
+
"""Get a rich Text representation of the match for display."""
|
|
189
279
|
headline = prefix_with_style(str(self.match_length), style='number', root_style='decode.subheading')
|
|
190
280
|
headline.append(f" bytes matching ")
|
|
191
281
|
headline.append(f"{self.label} ", style=ALERT_STYLE if self.highlight_style == ALERT_STYLE else 'regex')
|
|
@@ -193,4 +283,5 @@ class BytesMatch:
|
|
|
193
283
|
return headline + self.location()
|
|
194
284
|
|
|
195
285
|
def __str__(self):
|
|
286
|
+
"""Plain text (no rich colors) representation of the match for display."""
|
|
196
287
|
return self.__rich__().plain
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for Yaralyzer.
|
|
3
|
+
"""
|
|
1
4
|
import logging
|
|
2
5
|
from argparse import ArgumentParser, Namespace
|
|
3
6
|
from os import environ
|
|
@@ -15,16 +18,19 @@ MEGABYTE = 1024 * KILOBYTE
|
|
|
15
18
|
|
|
16
19
|
def config_var_name(env_var: str) -> str:
|
|
17
20
|
"""
|
|
18
|
-
Get the name of env_var and strip off 'YARALYZER_'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
Get the name of env_var and strip off 'YARALYZER_' prefix.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
$ SURROUNDING_BYTES_ENV_VAR = 'YARALYZER_SURROUNDING_BYTES'
|
|
25
|
+
$ config_var_name(SURROUNDING_BYTES_ENV_VAR) => 'SURROUNDING_BYTES'
|
|
26
|
+
|
|
21
27
|
"""
|
|
22
28
|
env_var = env_var.removeprefix("YARALYZER_")
|
|
23
29
|
return f'{env_var=}'.partition('=')[0]
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def is_env_var_set_and_not_false(var_name):
|
|
27
|
-
"""
|
|
33
|
+
"""Return True if var_name is not empty and set to anything other than 'false' (capitalization agnostic)."""
|
|
28
34
|
if var_name in environ:
|
|
29
35
|
var_value = environ[var_name]
|
|
30
36
|
return var_value is not None and len(var_value) > 0 and var_value.lower() != 'false'
|
|
@@ -33,11 +39,13 @@ def is_env_var_set_and_not_false(var_name):
|
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
def is_invoked_by_pytest():
|
|
36
|
-
"""Return true if pytest is running"""
|
|
42
|
+
"""Return true if pytest is running."""
|
|
37
43
|
return is_env_var_set_and_not_false(PYTEST_FLAG)
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
class YaralyzerConfig:
|
|
47
|
+
"""Handles parsing of command line args and environment variables for Yaralyzer."""
|
|
48
|
+
|
|
41
49
|
# Passed through to yara.set_config()
|
|
42
50
|
DEFAULT_MAX_MATCH_LENGTH = 100 * KILOBYTE
|
|
43
51
|
DEFAULT_YARA_STACK_SIZE = 2 * 65536
|
|
@@ -76,11 +84,13 @@ class YaralyzerConfig:
|
|
|
76
84
|
|
|
77
85
|
@classmethod
|
|
78
86
|
def set_argument_parser(cls, parser: ArgumentParser) -> None:
|
|
87
|
+
"""Sets the _argument_parser instance variable that will be used to parse command line args."""
|
|
79
88
|
cls._argument_parser: ArgumentParser = parser
|
|
80
89
|
cls._argparse_keys: List[str] = sorted([action.dest for action in parser._actions])
|
|
81
90
|
|
|
82
91
|
@classmethod
|
|
83
92
|
def set_args(cls, args: Namespace) -> None:
|
|
93
|
+
"""Set the args class instance variable and update args with any environment variable overrides."""
|
|
84
94
|
cls.args = args
|
|
85
95
|
|
|
86
96
|
for option in cls._argparse_keys:
|
|
@@ -105,9 +115,11 @@ class YaralyzerConfig:
|
|
|
105
115
|
|
|
106
116
|
@classmethod
|
|
107
117
|
def set_default_args(cls):
|
|
118
|
+
"""Set args to their defaults as if parsed from the command line."""
|
|
108
119
|
cls.set_args(cls._argument_parser.parse_args(['dummy']))
|
|
109
120
|
|
|
110
121
|
@classmethod
|
|
111
122
|
def get_default_arg(cls, arg: str) -> Any:
|
|
123
|
+
"""Return the default value for arg as defined by a DEFAULT_ style class variable."""
|
|
112
124
|
default_var = f"DEFAULT_{arg.upper()}"
|
|
113
125
|
return vars(cls).get(default_var)
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Class to handle attempting to decode a chunk of bytes into strings with various possible encodings.
|
|
3
|
-
Leverages the chardet library to both guide what encodings are attempted as well as to rank decodings
|
|
4
|
-
in the results.
|
|
5
|
-
"""
|
|
1
|
+
"""BytesDecoder class for attempting to decode bytes with various encodings."""
|
|
6
2
|
from collections import defaultdict
|
|
7
3
|
from copy import deepcopy
|
|
8
4
|
from operator import attrgetter
|
|
@@ -34,7 +30,33 @@ SCORE_SCALER = 100.0
|
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
class BytesDecoder:
|
|
33
|
+
"""
|
|
34
|
+
Class to handle attempting to decode a chunk of bytes into strings with various possible encodings.
|
|
35
|
+
|
|
36
|
+
Leverages the chardet library to both guide what encodings are attempted as well as to rank decodings
|
|
37
|
+
in the results.
|
|
38
|
+
"""
|
|
39
|
+
|
|
37
40
|
def __init__(self, bytes_match: 'BytesMatch', label: Optional[str] = None) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Initialize a BytesDecoder for attempting to decode a chunk of bytes using various encodings.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
bytes_match (BytesMatch): The BytesMatch object containing the bytes to decode and match metadata.
|
|
46
|
+
label (Optional[str], optional): Optional label for this decoding attempt. Defaults to the match label.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
bytes_match (BytesMatch): The BytesMatch instance being decoded.
|
|
50
|
+
bytes (bytes): The bytes (including surrounding context) to decode.
|
|
51
|
+
label (str): Label for this decoding attempt.
|
|
52
|
+
was_match_decodable (dict): Tracks successful decodes per encoding.
|
|
53
|
+
was_match_force_decoded (dict): Tracks forced decodes per encoding.
|
|
54
|
+
was_match_undecodable (dict): Tracks failed decodes per encoding.
|
|
55
|
+
decoded_strings (dict): Maps encoding to decoded string.
|
|
56
|
+
undecoded_rows (list): Stores undecoded table rows.
|
|
57
|
+
decodings (list): List of DecodingAttempt objects for each encoding tried.
|
|
58
|
+
encoding_detector (EncodingDetector): Used to detect and assess possible encodings.
|
|
59
|
+
"""
|
|
38
60
|
self.bytes_match = bytes_match
|
|
39
61
|
self.bytes = bytes_match.surrounding_bytes
|
|
40
62
|
self.label = label or bytes_match.label
|
|
@@ -51,7 +73,7 @@ class BytesDecoder:
|
|
|
51
73
|
self.encoding_detector = EncodingDetector(self.bytes)
|
|
52
74
|
|
|
53
75
|
def __rich_console__(self, _console: Console, options: ConsoleOptions) -> RenderResult:
|
|
54
|
-
"""Rich object generator (see Rich console docs)"""
|
|
76
|
+
"""Rich object generator (see Rich console docs)."""
|
|
55
77
|
yield NewLine(2)
|
|
56
78
|
yield Align(self._decode_attempt_subheading(), CENTER)
|
|
57
79
|
|
|
@@ -70,7 +92,7 @@ class BytesDecoder:
|
|
|
70
92
|
yield Align(self.bytes_match.bytes_hashes_table(), CENTER, style='dim')
|
|
71
93
|
|
|
72
94
|
def _build_decodings_table(self, suppress_decodes: bool = False) -> Table:
|
|
73
|
-
"""First rows are the raw / hex views of the bytes, next rows are the attempted decodings"""
|
|
95
|
+
"""First rows are the raw / hex views of the bytes, next rows are the attempted decodings."""
|
|
74
96
|
self.table = new_decoding_attempts_table(self.bytes_match)
|
|
75
97
|
|
|
76
98
|
# Add the encoding rows to the table if not suppressed
|
|
@@ -115,7 +137,7 @@ class BytesDecoder:
|
|
|
115
137
|
return Panel(headline, style='decode.subheading', expand=False)
|
|
116
138
|
|
|
117
139
|
def _track_decode_stats(self) -> None:
|
|
118
|
-
"""Track stats about successful vs. forced vs. failed decode attempts"""
|
|
140
|
+
"""Track stats about successful vs. forced vs. failed decode attempts."""
|
|
119
141
|
for decoding in self.decodings:
|
|
120
142
|
if decoding.failed_to_decode:
|
|
121
143
|
self.was_match_undecodable[decoding.encoding] += 1
|
|
@@ -162,7 +184,7 @@ class BytesDecoder:
|
|
|
162
184
|
|
|
163
185
|
|
|
164
186
|
def _build_encodings_metric_dict():
|
|
165
|
-
"""One key for each key in ENCODINGS_TO_ATTEMPT, values are all 0"""
|
|
187
|
+
"""One key for each key in ENCODINGS_TO_ATTEMPT, values are all 0."""
|
|
166
188
|
metrics_dict = defaultdict(lambda: 0)
|
|
167
189
|
|
|
168
190
|
for encoding in ENCODINGS_TO_ATTEMPT.keys():
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Class to manage attempting to decode a chunk of bytes into strings with a given encoding.
|
|
3
|
-
"""
|
|
1
|
+
"""Class to manage attempting to decode a chunk of bytes into strings with a given encoding."""
|
|
4
2
|
from sys import byteorder
|
|
5
3
|
from typing import Optional
|
|
6
4
|
|
|
7
5
|
from rich.markup import escape
|
|
8
6
|
from rich.text import Text
|
|
9
7
|
|
|
10
|
-
from yaralyzer.bytes_match import BytesMatch #
|
|
8
|
+
from yaralyzer.bytes_match import BytesMatch # Formerly caused circular import issues
|
|
11
9
|
from yaralyzer.encoding_detection.character_encodings import (ENCODINGS_TO_ATTEMPT, SINGLE_BYTE_ENCODINGS,
|
|
12
10
|
UTF_8, encoding_width, is_wide_utf)
|
|
13
11
|
from yaralyzer.helpers.bytes_helper import clean_byte_string, truncate_for_encoding
|
|
@@ -17,6 +15,8 @@ from yaralyzer.util.logging import log
|
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class DecodingAttempt:
|
|
18
|
+
"""Class to manage attempting to decode a chunk of bytes into strings with a given encoding."""
|
|
19
|
+
|
|
20
20
|
def __init__(self, bytes_match: 'BytesMatch', encoding: str) -> None:
|
|
21
21
|
# Args
|
|
22
22
|
self.bytes = bytes_match.surrounding_bytes
|
|
@@ -31,7 +31,7 @@ class DecodingAttempt:
|
|
|
31
31
|
self.decoded_string = self._decode_bytes()
|
|
32
32
|
|
|
33
33
|
def is_wide_utf_encoding(self) -> bool:
|
|
34
|
-
"""Returns True if the encoding is UTF-16 or UTF-32"""
|
|
34
|
+
"""Returns True if the encoding is UTF-16 or UTF-32."""
|
|
35
35
|
return is_wide_utf(self.encoding)
|
|
36
36
|
|
|
37
37
|
def _decode_bytes(self) -> Text:
|
|
@@ -58,7 +58,7 @@ class DecodingAttempt:
|
|
|
58
58
|
return self._custom_decode()
|
|
59
59
|
|
|
60
60
|
def _custom_decode(self) -> Text:
|
|
61
|
-
"""Returns a Text obj representing an attempt to force a UTF-8 encoding upon an array of bytes"""
|
|
61
|
+
"""Returns a Text obj representing an attempt to force a UTF-8 encoding upon an array of bytes."""
|
|
62
62
|
log.info(f"Custom decoding {self.bytes_match} with {self.encoding}...")
|
|
63
63
|
unprintable_char_map = ENCODINGS_TO_ATTEMPT.get(self.encoding)
|
|
64
64
|
output = Text('', style='bytes.decoded')
|
|
@@ -146,7 +146,7 @@ class DecodingAttempt:
|
|
|
146
146
|
return self._failed_to_decode_msg_txt(last_exception)
|
|
147
147
|
|
|
148
148
|
def _to_rich_text(self, _string: str, bytes_offset: int = 0) -> Text:
|
|
149
|
-
"""Convert a decoded string to highlighted Text representation"""
|
|
149
|
+
"""Convert a decoded string to highlighted Text representation."""
|
|
150
150
|
# Adjust where we start the highlighting given the multibyte nature of the encodings
|
|
151
151
|
log.debug(f"Stepping through {self.encoding} encoded string...")
|
|
152
152
|
txt = Text('', style=self.bytes_match.style_at_position(0))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Help with chardet library.
|
|
3
3
|
"""
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
@@ -14,7 +14,13 @@ LANGUAGE = 'language'
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class EncodingAssessment:
|
|
17
|
+
"""Class to smooth some of the rough edges around the dicts returned by chardet.detect_all()"""
|
|
18
|
+
|
|
17
19
|
def __init__(self, assessment: dict) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Args:
|
|
22
|
+
assessment (dict): The dict returned by chardet.detect_all().
|
|
23
|
+
"""
|
|
18
24
|
self.assessment = assessment
|
|
19
25
|
self.encoding = assessment[ENCODING].lower()
|
|
20
26
|
|
|
@@ -27,7 +33,7 @@ class EncodingAssessment:
|
|
|
27
33
|
self.set_encoding_label(self.language.title() if self.language else None)
|
|
28
34
|
|
|
29
35
|
@classmethod
|
|
30
|
-
def dummy_encoding_assessment(cls, encoding) -> 'EncodingAssessment':
|
|
36
|
+
def dummy_encoding_assessment(cls, encoding: str) -> 'EncodingAssessment':
|
|
31
37
|
"""Generate an empty EncodingAssessment to use as a dummy when chardet gives us nothing."""
|
|
32
38
|
assessment = cls({ENCODING: encoding, CONFIDENCE: 0.0})
|
|
33
39
|
assessment.confidence_text = Text('none', 'no_attempt')
|