pyjsclear 0.1.3__tar.gz → 0.1.4__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.
- {pyjsclear-0.1.3/pyjsclear.egg-info → pyjsclear-0.1.4}/PKG-INFO +1 -1
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/__init__.py +7 -7
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/__main__.py +4 -3
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/deobfuscator.py +47 -16
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/generator.py +111 -110
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/parser.py +2 -2
- pyjsclear-0.1.4/pyjsclear/scope.py +505 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/aa_decode.py +12 -12
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/anti_tamper.py +12 -15
- pyjsclear-0.1.4/pyjsclear/transforms/base.py +58 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/class_static_resolver.py +27 -21
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/class_string_decoder.py +64 -63
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/cleanup.py +54 -43
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/constant_prop.py +41 -32
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/control_flow.py +94 -97
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_branch.py +9 -9
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_class_props.py +55 -52
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_expressions.py +5 -5
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_object_props.py +29 -29
- pyjsclear-0.1.4/pyjsclear/transforms/else_if_flatten.py +32 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/enum_resolver.py +15 -15
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/eval_unpack.py +28 -27
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/expression_simplifier.py +96 -78
- pyjsclear-0.1.4/pyjsclear/transforms/global_alias.py +128 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_escapes.py +10 -13
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_numerics.py +2 -2
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/jj_decode.py +159 -157
- pyjsclear-0.1.4/pyjsclear/transforms/jsfuck_decode.py +610 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/logical_to_if.py +57 -57
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/member_chain_resolver.py +51 -73
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/noop_calls.py +22 -20
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/nullish_coalescing.py +39 -28
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/object_packer.py +11 -13
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/object_simplifier.py +44 -41
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/optional_chaining.py +44 -43
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/property_simplifier.py +4 -4
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/proxy_functions.py +38 -35
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/reassignment.py +26 -17
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/require_inliner.py +13 -17
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/sequence_splitter.py +59 -61
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/single_use_vars.py +30 -21
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/string_revealer.py +99 -88
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/unreachable_code.py +14 -13
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/unused_vars.py +22 -19
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/variable_renamer.py +108 -108
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/xor_string_decode.py +39 -39
- pyjsclear-0.1.4/pyjsclear/traverser.py +434 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/ast_helpers.py +36 -36
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/string_decoders.py +18 -19
- {pyjsclear-0.1.3 → pyjsclear-0.1.4/pyjsclear.egg-info}/PKG-INFO +1 -1
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/tests/test_regression.py +25 -28
- pyjsclear-0.1.3/pyjsclear/scope.py +0 -309
- pyjsclear-0.1.3/pyjsclear/transforms/base.py +0 -24
- pyjsclear-0.1.3/pyjsclear/transforms/else_if_flatten.py +0 -32
- pyjsclear-0.1.3/pyjsclear/transforms/global_alias.py +0 -117
- pyjsclear-0.1.3/pyjsclear/transforms/jsfuck_decode.py +0 -593
- pyjsclear-0.1.3/pyjsclear/traverser.py +0 -195
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/LICENSE +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/MANIFEST.in +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/NOTICE +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/README.md +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/THIRD_PARTY_LICENSES.md +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/__init__.py +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/__init__.py +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/SOURCES.txt +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/dependency_links.txt +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/entry_points.txt +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/requires.txt +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/top_level.txt +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyproject.toml +0 -0
- {pyjsclear-0.1.3 → pyjsclear-0.1.4}/setup.cfg +0 -0
|
@@ -8,10 +8,10 @@ Python package.
|
|
|
8
8
|
from .deobfuscator import Deobfuscator
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
__version__ = '0.1.
|
|
11
|
+
__version__ = '0.1.4'
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def deobfuscate(code, max_iterations=50):
|
|
14
|
+
def deobfuscate(code: str, max_iterations: int = 50) -> str:
|
|
15
15
|
"""Deobfuscate JavaScript code. Returns cleaned source.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
@@ -24,7 +24,7 @@ def deobfuscate(code, max_iterations=50):
|
|
|
24
24
|
return Deobfuscator(code, max_iterations=max_iterations).execute()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def deobfuscate_file(input_path, output_path=None, max_iterations=50):
|
|
27
|
+
def deobfuscate_file(input_path: str, output_path: str | None = None, max_iterations: int = 50) -> str | bool:
|
|
28
28
|
"""Deobfuscate a JavaScript file.
|
|
29
29
|
|
|
30
30
|
Args:
|
|
@@ -35,13 +35,13 @@ def deobfuscate_file(input_path, output_path=None, max_iterations=50):
|
|
|
35
35
|
Returns:
|
|
36
36
|
True if content changed (when output_path given), or the deobfuscated string.
|
|
37
37
|
"""
|
|
38
|
-
with open(input_path, 'r', errors='replace') as
|
|
39
|
-
code =
|
|
38
|
+
with open(input_path, 'r', errors='replace') as input_file:
|
|
39
|
+
code = input_file.read()
|
|
40
40
|
|
|
41
41
|
result = deobfuscate(code, max_iterations=max_iterations)
|
|
42
42
|
|
|
43
43
|
if output_path:
|
|
44
|
-
with open(output_path, 'w') as
|
|
45
|
-
|
|
44
|
+
with open(output_path, 'w') as output_file:
|
|
45
|
+
output_file.write(result)
|
|
46
46
|
return result != code
|
|
47
47
|
return result
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
from . import deobfuscate
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def main():
|
|
9
|
+
def main() -> None:
|
|
10
10
|
parser = argparse.ArgumentParser(description='Deobfuscate JavaScript files.')
|
|
11
11
|
parser.add_argument('input', help='Input JS file (use - for stdin)')
|
|
12
12
|
parser.add_argument('-o', '--output', help='Output file (default: stdout)')
|
|
@@ -29,8 +29,9 @@ def main():
|
|
|
29
29
|
if args.output:
|
|
30
30
|
with open(args.output, 'w') as output_file:
|
|
31
31
|
output_file.write(result)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
sys.stdout.write(result)
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
if __name__ == '__main__':
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from .generator import generate
|
|
4
4
|
from .parser import parse
|
|
5
|
+
from .scope import build_scope_tree
|
|
6
|
+
from .transforms.aa_decode import aa_decode
|
|
7
|
+
from .transforms.aa_decode import is_aa_encoded
|
|
5
8
|
from .transforms.anti_tamper import AntiTamperRemover
|
|
6
9
|
from .transforms.class_static_resolver import ClassStaticResolver
|
|
7
10
|
from .transforms.class_string_decoder import ClassStringDecoder
|
|
@@ -26,8 +29,6 @@ from .transforms.global_alias import GlobalAliasInliner
|
|
|
26
29
|
from .transforms.hex_escapes import HexEscapes
|
|
27
30
|
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
28
31
|
from .transforms.hex_numerics import HexNumerics
|
|
29
|
-
from .transforms.aa_decode import aa_decode
|
|
30
|
-
from .transforms.aa_decode import is_aa_encoded
|
|
31
32
|
from .transforms.jj_decode import is_jj_encoded
|
|
32
33
|
from .transforms.jj_decode import jj_decode
|
|
33
34
|
from .transforms.jsfuck_decode import is_jsfuck
|
|
@@ -53,6 +54,22 @@ from .transforms.xor_string_decode import XorStringDecoder
|
|
|
53
54
|
from .traverser import simple_traverse
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
# Transforms that use build_scope_tree and benefit from cached scope
|
|
58
|
+
_SCOPE_TRANSFORMS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
ConstantProp,
|
|
61
|
+
SingleUseVarInliner,
|
|
62
|
+
ReassignmentRemover,
|
|
63
|
+
ProxyFunctionInliner,
|
|
64
|
+
UnusedVariableRemover,
|
|
65
|
+
ObjectSimplifier,
|
|
66
|
+
StringRevealer,
|
|
67
|
+
VariableRenamer,
|
|
68
|
+
VarToConst,
|
|
69
|
+
LetToConst,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
56
73
|
# StringRevealer runs first to handle string arrays before other transforms
|
|
57
74
|
# modify the wrapper function structure.
|
|
58
75
|
# Remaining transforms follow obfuscator-io-deobfuscator order.
|
|
@@ -106,26 +123,26 @@ _LITE_MAX_ITERATIONS = 10
|
|
|
106
123
|
_NODE_COUNT_LIMIT = 50_000 # Skip ControlFlowRecoverer above this
|
|
107
124
|
|
|
108
125
|
|
|
109
|
-
def _count_nodes(ast):
|
|
126
|
+
def _count_nodes(ast: dict) -> int:
|
|
110
127
|
"""Count total AST nodes."""
|
|
111
128
|
count = 0
|
|
112
129
|
|
|
113
|
-
def
|
|
130
|
+
def increment_count(node: dict, parent: dict | None) -> None:
|
|
114
131
|
nonlocal count
|
|
115
132
|
count += 1
|
|
116
133
|
|
|
117
|
-
simple_traverse(ast,
|
|
134
|
+
simple_traverse(ast, increment_count)
|
|
118
135
|
return count
|
|
119
136
|
|
|
120
137
|
|
|
121
138
|
class Deobfuscator:
|
|
122
139
|
"""Multi-pass JavaScript deobfuscator."""
|
|
123
140
|
|
|
124
|
-
def __init__(self, code, max_iterations=50):
|
|
141
|
+
def __init__(self, code: str, max_iterations: int = 50) -> None:
|
|
125
142
|
self.original_code = code
|
|
126
143
|
self.max_iterations = max_iterations
|
|
127
144
|
|
|
128
|
-
def _run_pre_passes(self, code):
|
|
145
|
+
def _run_pre_passes(self, code: str) -> str | None:
|
|
129
146
|
"""Run encoding detection and eval unpacking pre-passes.
|
|
130
147
|
|
|
131
148
|
Returns decoded code if an encoding/packing was detected and decoded,
|
|
@@ -160,7 +177,7 @@ class Deobfuscator:
|
|
|
160
177
|
# Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
|
|
161
178
|
_MAX_OUTER_CYCLES = 5
|
|
162
179
|
|
|
163
|
-
def execute(self):
|
|
180
|
+
def execute(self) -> str:
|
|
164
181
|
"""Run all transforms and return cleaned source."""
|
|
165
182
|
code = self.original_code
|
|
166
183
|
|
|
@@ -238,7 +255,7 @@ class Deobfuscator:
|
|
|
238
255
|
# but also recursive. Return best result so far.
|
|
239
256
|
return previous_code
|
|
240
257
|
|
|
241
|
-
def _run_ast_transforms(self, ast, code_size=0):
|
|
258
|
+
def _run_ast_transforms(self, ast: dict, code_size: int = 0) -> bool:
|
|
242
259
|
"""Run all AST transform passes. Returns True if any transform changed the AST."""
|
|
243
260
|
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
244
261
|
|
|
@@ -259,26 +276,40 @@ class Deobfuscator:
|
|
|
259
276
|
# Track which transforms are no longer productive
|
|
260
277
|
skip_transforms = set()
|
|
261
278
|
|
|
279
|
+
# Cache scope tree across transforms — only rebuild when a transform
|
|
280
|
+
# that modifies bindings returns changed=True
|
|
281
|
+
scope_tree = None
|
|
282
|
+
node_scope = None
|
|
283
|
+
scope_dirty = True # Start dirty to build on first use
|
|
284
|
+
|
|
262
285
|
# Multi-pass transform loop
|
|
263
286
|
any_transform_changed = False
|
|
264
|
-
for
|
|
287
|
+
for iteration in range(max_iterations):
|
|
265
288
|
modified = False
|
|
266
289
|
for transform_class in transform_classes:
|
|
267
290
|
if transform_class in skip_transforms:
|
|
268
291
|
continue
|
|
269
292
|
try:
|
|
270
|
-
|
|
293
|
+
# Build scope tree lazily when needed by a scope-using transform
|
|
294
|
+
if transform_class in _SCOPE_TRANSFORMS and scope_dirty:
|
|
295
|
+
scope_tree, node_scope = build_scope_tree(ast)
|
|
296
|
+
scope_dirty = False
|
|
297
|
+
|
|
298
|
+
if transform_class in _SCOPE_TRANSFORMS:
|
|
299
|
+
transform = transform_class(ast, scope_tree=scope_tree, node_scope=node_scope)
|
|
300
|
+
else:
|
|
301
|
+
transform = transform_class(ast)
|
|
271
302
|
result = transform.execute()
|
|
272
303
|
except Exception:
|
|
273
304
|
continue
|
|
274
305
|
if result:
|
|
275
306
|
modified = True
|
|
276
307
|
any_transform_changed = True
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
# Any AST change invalidates the cached scope tree
|
|
309
|
+
scope_dirty = True
|
|
310
|
+
elif iteration > 0:
|
|
311
|
+
# Skip transforms that haven't changed anything after the first pass
|
|
312
|
+
skip_transforms.add(transform_class)
|
|
282
313
|
|
|
283
314
|
if not modified:
|
|
284
315
|
break
|