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.
Files changed (71) hide show
  1. {pyjsclear-0.1.3/pyjsclear.egg-info → pyjsclear-0.1.4}/PKG-INFO +1 -1
  2. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/__init__.py +7 -7
  3. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/__main__.py +4 -3
  4. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/deobfuscator.py +47 -16
  5. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/generator.py +111 -110
  6. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/parser.py +2 -2
  7. pyjsclear-0.1.4/pyjsclear/scope.py +505 -0
  8. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/aa_decode.py +12 -12
  9. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/anti_tamper.py +12 -15
  10. pyjsclear-0.1.4/pyjsclear/transforms/base.py +58 -0
  11. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/class_static_resolver.py +27 -21
  12. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/class_string_decoder.py +64 -63
  13. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/cleanup.py +54 -43
  14. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/constant_prop.py +41 -32
  15. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/control_flow.py +94 -97
  16. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_branch.py +9 -9
  17. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_class_props.py +55 -52
  18. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_expressions.py +5 -5
  19. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_object_props.py +29 -29
  20. pyjsclear-0.1.4/pyjsclear/transforms/else_if_flatten.py +32 -0
  21. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/enum_resolver.py +15 -15
  22. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/eval_unpack.py +28 -27
  23. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/expression_simplifier.py +96 -78
  24. pyjsclear-0.1.4/pyjsclear/transforms/global_alias.py +128 -0
  25. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_escapes.py +10 -13
  26. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_numerics.py +2 -2
  27. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/jj_decode.py +159 -157
  28. pyjsclear-0.1.4/pyjsclear/transforms/jsfuck_decode.py +610 -0
  29. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/logical_to_if.py +57 -57
  30. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/member_chain_resolver.py +51 -73
  31. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/noop_calls.py +22 -20
  32. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/nullish_coalescing.py +39 -28
  33. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/object_packer.py +11 -13
  34. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/object_simplifier.py +44 -41
  35. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/optional_chaining.py +44 -43
  36. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/property_simplifier.py +4 -4
  37. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/proxy_functions.py +38 -35
  38. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/reassignment.py +26 -17
  39. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/require_inliner.py +13 -17
  40. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/sequence_splitter.py +59 -61
  41. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/single_use_vars.py +30 -21
  42. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/string_revealer.py +99 -88
  43. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/unreachable_code.py +14 -13
  44. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/unused_vars.py +22 -19
  45. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/variable_renamer.py +108 -108
  46. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/xor_string_decode.py +39 -39
  47. pyjsclear-0.1.4/pyjsclear/traverser.py +434 -0
  48. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/ast_helpers.py +36 -36
  49. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/string_decoders.py +18 -19
  50. {pyjsclear-0.1.3 → pyjsclear-0.1.4/pyjsclear.egg-info}/PKG-INFO +1 -1
  51. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/tests/test_regression.py +25 -28
  52. pyjsclear-0.1.3/pyjsclear/scope.py +0 -309
  53. pyjsclear-0.1.3/pyjsclear/transforms/base.py +0 -24
  54. pyjsclear-0.1.3/pyjsclear/transforms/else_if_flatten.py +0 -32
  55. pyjsclear-0.1.3/pyjsclear/transforms/global_alias.py +0 -117
  56. pyjsclear-0.1.3/pyjsclear/transforms/jsfuck_decode.py +0 -593
  57. pyjsclear-0.1.3/pyjsclear/traverser.py +0 -195
  58. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/LICENSE +0 -0
  59. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/MANIFEST.in +0 -0
  60. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/NOTICE +0 -0
  61. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/README.md +0 -0
  62. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/THIRD_PARTY_LICENSES.md +0 -0
  63. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/transforms/__init__.py +0 -0
  64. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear/utils/__init__.py +0 -0
  65. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/SOURCES.txt +0 -0
  66. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/dependency_links.txt +0 -0
  67. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/entry_points.txt +0 -0
  68. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/requires.txt +0 -0
  69. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyjsclear.egg-info/top_level.txt +0 -0
  70. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/pyproject.toml +0 -0
  71. {pyjsclear-0.1.3 → pyjsclear-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjsclear
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Pure Python JavaScript deobfuscator
5
5
  Author-email: Intezer Labs <info@intezer.com>
6
6
  License-Expression: Apache-2.0
@@ -8,10 +8,10 @@ Python package.
8
8
  from .deobfuscator import Deobfuscator
9
9
 
10
10
 
11
- __version__ = '0.1.3'
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 f:
39
- code = f.read()
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 f:
45
- f.write(result)
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
- else:
33
- sys.stdout.write(result)
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 cb(node, parent):
130
+ def increment_count(node: dict, parent: dict | None) -> None:
114
131
  nonlocal count
115
132
  count += 1
116
133
 
117
- simple_traverse(ast, cb)
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 i in range(max_iterations):
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
- transform = transform_class(ast)
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
- else:
278
- # If a transform didn't change anything after the first pass,
279
- # skip it in subsequent iterations
280
- if i > 0:
281
- skip_transforms.add(transform_class)
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