pyjsclear 0.1.1__tar.gz → 0.1.3__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 (72) hide show
  1. pyjsclear-0.1.3/NOTICE +28 -0
  2. pyjsclear-0.1.3/PKG-INFO +124 -0
  3. pyjsclear-0.1.3/README.md +98 -0
  4. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/THIRD_PARTY_LICENSES.md +27 -0
  5. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/__init__.py +2 -2
  6. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/deobfuscator.py +115 -20
  7. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/generator.py +6 -1
  8. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/scope.py +107 -81
  9. pyjsclear-0.1.3/pyjsclear/transforms/aa_decode.py +150 -0
  10. pyjsclear-0.1.3/pyjsclear/transforms/class_static_resolver.py +206 -0
  11. pyjsclear-0.1.3/pyjsclear/transforms/class_string_decoder.py +332 -0
  12. pyjsclear-0.1.3/pyjsclear/transforms/cleanup.py +272 -0
  13. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_branch.py +18 -0
  14. pyjsclear-0.1.3/pyjsclear/transforms/dead_class_props.py +251 -0
  15. pyjsclear-0.1.3/pyjsclear/transforms/dead_expressions.py +29 -0
  16. pyjsclear-0.1.3/pyjsclear/transforms/dead_object_props.py +150 -0
  17. pyjsclear-0.1.3/pyjsclear/transforms/else_if_flatten.py +32 -0
  18. pyjsclear-0.1.3/pyjsclear/transforms/enum_resolver.py +175 -0
  19. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/eval_unpack.py +3 -52
  20. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/expression_simplifier.py +106 -0
  21. pyjsclear-0.1.3/pyjsclear/transforms/global_alias.py +117 -0
  22. pyjsclear-0.1.3/pyjsclear/transforms/hex_numerics.py +30 -0
  23. pyjsclear-0.1.3/pyjsclear/transforms/jj_decode.py +718 -0
  24. pyjsclear-0.1.3/pyjsclear/transforms/jsfuck_decode.py +593 -0
  25. pyjsclear-0.1.3/pyjsclear/transforms/member_chain_resolver.py +171 -0
  26. pyjsclear-0.1.3/pyjsclear/transforms/noop_calls.py +77 -0
  27. pyjsclear-0.1.3/pyjsclear/transforms/nullish_coalescing.py +98 -0
  28. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/object_simplifier.py +7 -5
  29. pyjsclear-0.1.3/pyjsclear/transforms/optional_chaining.py +152 -0
  30. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/property_simplifier.py +29 -4
  31. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/proxy_functions.py +44 -2
  32. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/reassignment.py +5 -0
  33. pyjsclear-0.1.3/pyjsclear/transforms/require_inliner.py +81 -0
  34. pyjsclear-0.1.3/pyjsclear/transforms/single_use_vars.py +155 -0
  35. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/string_revealer.py +2 -0
  36. pyjsclear-0.1.3/pyjsclear/transforms/unreachable_code.py +37 -0
  37. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/unused_vars.py +3 -2
  38. pyjsclear-0.1.3/pyjsclear/transforms/variable_renamer.py +511 -0
  39. pyjsclear-0.1.3/pyjsclear/transforms/xor_string_decode.py +228 -0
  40. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/ast_helpers.py +72 -2
  41. pyjsclear-0.1.3/pyjsclear.egg-info/PKG-INFO +124 -0
  42. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/SOURCES.txt +19 -0
  43. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/tests/test_regression.py +288 -13
  44. pyjsclear-0.1.1/NOTICE +0 -19
  45. pyjsclear-0.1.1/PKG-INFO +0 -168
  46. pyjsclear-0.1.1/README.md +0 -142
  47. pyjsclear-0.1.1/pyjsclear/transforms/aa_decode.py +0 -83
  48. pyjsclear-0.1.1/pyjsclear/transforms/jj_decode.py +0 -91
  49. pyjsclear-0.1.1/pyjsclear/transforms/jsfuck_decode.py +0 -93
  50. pyjsclear-0.1.1/pyjsclear.egg-info/PKG-INFO +0 -168
  51. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/LICENSE +0 -0
  52. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/MANIFEST.in +0 -0
  53. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/__main__.py +0 -0
  54. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/parser.py +0 -0
  55. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/__init__.py +0 -0
  56. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/anti_tamper.py +0 -0
  57. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/base.py +0 -0
  58. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/constant_prop.py +0 -0
  59. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/control_flow.py +0 -0
  60. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_escapes.py +0 -0
  61. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/logical_to_if.py +0 -0
  62. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/object_packer.py +0 -0
  63. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/sequence_splitter.py +0 -0
  64. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/traverser.py +0 -0
  65. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/__init__.py +0 -0
  66. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/string_decoders.py +0 -0
  67. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/dependency_links.txt +0 -0
  68. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/entry_points.txt +0 -0
  69. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/requires.txt +0 -0
  70. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/top_level.txt +0 -0
  71. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyproject.toml +0 -0
  72. {pyjsclear-0.1.1 → pyjsclear-0.1.3}/setup.cfg +0 -0
pyjsclear-0.1.3/NOTICE ADDED
@@ -0,0 +1,28 @@
1
+ PyJSClear
2
+ Copyright 2026 Intezer Labs
3
+
4
+ This product is a derivative work based on the following projects:
5
+
6
+ 1. obfuscator-io-deobfuscator (v1.0.6)
7
+ Copyright Ben (ben-sb)
8
+ Licensed under the Apache License, Version 2.0
9
+ https://github.com/ben-sb/obfuscator-io-deobfuscator
10
+
11
+ 2. javascript-deobfuscator
12
+ Copyright Ben (ben-sb)
13
+ Licensed under the Apache License, Version 2.0
14
+ https://github.com/ben-sb/javascript-deobfuscator
15
+
16
+ 3. webcrack (v2.14.1)
17
+ Copyright (c) 2023 j4k0xb
18
+ Licensed under the MIT License
19
+ https://github.com/j4k0xb/webcrack
20
+
21
+ This Python library re-implements the deobfuscation algorithms and transform
22
+ logic from the above projects in pure Python. No source code was directly
23
+ copied; the implementations were written from scratch following the same
24
+ algorithmic approaches.
25
+
26
+ Test dataset: obfuscated JavaScript samples from the JSIMPLIFIER dataset
27
+ (https://zenodo.org/records/17531662) are included in tests/resources/ for
28
+ evaluation purposes only. They are not part of the distributed package.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyjsclear
3
+ Version: 0.1.3
4
+ Summary: Pure Python JavaScript deobfuscator
5
+ Author-email: Intezer Labs <info@intezer.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/intezer/PyJSClear
8
+ Project-URL: Repository, https://github.com/intezer/PyJSClear
9
+ Project-URL: Issues, https://github.com/intezer/PyJSClear/issues
10
+ Keywords: javascript,deobfuscator,deobfuscation,security,malware-analysis,ast
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ License-File: NOTICE
24
+ Requires-Dist: esprima2>=5.0.1
25
+ Dynamic: license-file
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/intezer/PyJSClear/main/PyJSClear.png" alt="PyJSClear" width="200">
29
+ </p>
30
+
31
+ # PyJSClear
32
+
33
+ Pure Python JavaScript deobfuscator.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install pyjsclear
39
+ ```
40
+
41
+ For development:
42
+
43
+ ```bash
44
+ git clone https://github.com/intezer/PyJSClear.git
45
+ cd PyJSClear
46
+ pip install -e .
47
+ pip install pytest
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### Python API
53
+
54
+ ```python
55
+ from pyjsclear import deobfuscate, deobfuscate_file
56
+
57
+ # From a string
58
+ cleaned = deobfuscate(obfuscated_code)
59
+
60
+ # From a file
61
+ deobfuscate_file("input.js", "output.js")
62
+
63
+ # Or get the result as a string
64
+ cleaned = deobfuscate_file("input.js")
65
+ ```
66
+
67
+ ### Command line
68
+
69
+ ```bash
70
+ # File to stdout
71
+ pyjsclear input.js
72
+
73
+ # File to file
74
+ pyjsclear input.js -o output.js
75
+
76
+ # Stdin to stdout
77
+ cat input.js | pyjsclear -
78
+
79
+ # With custom iteration limit
80
+ pyjsclear input.js --max-iterations 20
81
+ ```
82
+
83
+ ## What it does
84
+
85
+ PyJSClear applies transforms in a multi-pass loop until the code
86
+ stabilizes (default limit: 50 iterations). A final one-shot pass renames
87
+ variables and converts var/let to const.
88
+
89
+ **Capabilities:**
90
+ - Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
91
+ - String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
92
+ - Constant propagation & reassignment elimination
93
+ - Dead code / dead branch / unreachable code removal
94
+ - Control-flow unflattening (switch-dispatch recovery)
95
+ - Proxy function & proxy object inlining
96
+ - Expression simplification & modern syntax recovery (?., ??)
97
+ - Anti-tamper / anti-debug removal
98
+ - Variable renaming (_0x… → readable names)
99
+
100
+ Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
101
+ that skips expensive transforms.
102
+
103
+ ## Limitations
104
+
105
+ - **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
106
+ - **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
107
+ - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
108
+
109
+ ## License
110
+
111
+ Apache License 2.0 — see [LICENSE](LICENSE).
112
+
113
+ This project is a derivative work based on
114
+ [obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
115
+ (Apache 2.0),
116
+ [javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
117
+ (Apache 2.0), and
118
+ [webcrack](https://github.com/j4k0xb/webcrack) (MIT).
119
+ See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
120
+ [NOTICE](NOTICE) for full attribution.
121
+
122
+ Test samples include obfuscated JavaScript from the
123
+ [JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
124
+ used solely for evaluation purposes.
@@ -0,0 +1,98 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/intezer/PyJSClear/main/PyJSClear.png" alt="PyJSClear" width="200">
3
+ </p>
4
+
5
+ # PyJSClear
6
+
7
+ Pure Python JavaScript deobfuscator.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install pyjsclear
13
+ ```
14
+
15
+ For development:
16
+
17
+ ```bash
18
+ git clone https://github.com/intezer/PyJSClear.git
19
+ cd PyJSClear
20
+ pip install -e .
21
+ pip install pytest
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Python API
27
+
28
+ ```python
29
+ from pyjsclear import deobfuscate, deobfuscate_file
30
+
31
+ # From a string
32
+ cleaned = deobfuscate(obfuscated_code)
33
+
34
+ # From a file
35
+ deobfuscate_file("input.js", "output.js")
36
+
37
+ # Or get the result as a string
38
+ cleaned = deobfuscate_file("input.js")
39
+ ```
40
+
41
+ ### Command line
42
+
43
+ ```bash
44
+ # File to stdout
45
+ pyjsclear input.js
46
+
47
+ # File to file
48
+ pyjsclear input.js -o output.js
49
+
50
+ # Stdin to stdout
51
+ cat input.js | pyjsclear -
52
+
53
+ # With custom iteration limit
54
+ pyjsclear input.js --max-iterations 20
55
+ ```
56
+
57
+ ## What it does
58
+
59
+ PyJSClear applies transforms in a multi-pass loop until the code
60
+ stabilizes (default limit: 50 iterations). A final one-shot pass renames
61
+ variables and converts var/let to const.
62
+
63
+ **Capabilities:**
64
+ - Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
65
+ - String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
66
+ - Constant propagation & reassignment elimination
67
+ - Dead code / dead branch / unreachable code removal
68
+ - Control-flow unflattening (switch-dispatch recovery)
69
+ - Proxy function & proxy object inlining
70
+ - Expression simplification & modern syntax recovery (?., ??)
71
+ - Anti-tamper / anti-debug removal
72
+ - Variable renaming (_0x… → readable names)
73
+
74
+ Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
75
+ that skips expensive transforms.
76
+
77
+ ## Limitations
78
+
79
+ - **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
80
+ - **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
81
+ - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
82
+
83
+ ## License
84
+
85
+ Apache License 2.0 — see [LICENSE](LICENSE).
86
+
87
+ This project is a derivative work based on
88
+ [obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
89
+ (Apache 2.0),
90
+ [javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
91
+ (Apache 2.0), and
92
+ [webcrack](https://github.com/j4k0xb/webcrack) (MIT).
93
+ See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
94
+ [NOTICE](NOTICE) for full attribution.
95
+
96
+ Test samples include obfuscated JavaScript from the
97
+ [JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
98
+ used solely for evaluation purposes.
@@ -237,3 +237,30 @@ https://github.com/ben-sb/javascript-deobfuscator/blob/master/LICENSE).
237
237
  **Features derived from this project:** hex escape decoding (`--he`),
238
238
  static array unpacking (`--su`), property access transformation (`--tp`).
239
239
 
240
+ ---
241
+
242
+ ## webcrack
243
+
244
+ - **Version:** 2.14.1
245
+ - **Author:** j4k0xb
246
+ - **Repository:** https://github.com/j4k0xb/webcrack
247
+ - **License:** MIT
248
+
249
+ See [licenses/LICENSE-webcrack](licenses/LICENSE-webcrack) for the full license text.
250
+
251
+ **Features derived from this project:** general deobfuscation transform
252
+ patterns and architecture reference.
253
+
254
+ ---
255
+
256
+ ## Test dataset
257
+
258
+ - **JSIMPLIFIER dataset**
259
+ - **Author:** Dongchao Zhou
260
+ - **Source:** https://zenodo.org/records/17531662
261
+ - **License:** GPL-3.0
262
+
263
+ Obfuscated JavaScript samples from this dataset are included in
264
+ `tests/resources/` for evaluation purposes only. They are not part of the
265
+ distributed package and no code from JSIMPLIFIER is incorporated into
266
+ PyJSClear.
@@ -2,13 +2,13 @@
2
2
 
3
3
  Combines functionality from obfuscator-io-deobfuscator (13 AST transforms)
4
4
  and javascript-deobfuscator (3 surface-cleanup modules) into a single
5
- Python package with no Node.js dependency.
5
+ Python package.
6
6
  """
7
7
 
8
8
  from .deobfuscator import Deobfuscator
9
9
 
10
10
 
11
- __version__ = '0.1.1'
11
+ __version__ = '0.1.3'
12
12
 
13
13
 
14
14
  def deobfuscate(code, max_iterations=50):
@@ -2,31 +2,54 @@
2
2
 
3
3
  from .generator import generate
4
4
  from .parser import parse
5
- from .transforms.aa_decode import aa_decode
6
- from .transforms.aa_decode import is_aa_encoded
7
5
  from .transforms.anti_tamper import AntiTamperRemover
6
+ from .transforms.class_static_resolver import ClassStaticResolver
7
+ from .transforms.class_string_decoder import ClassStringDecoder
8
+ from .transforms.cleanup import EmptyIfRemover
9
+ from .transforms.cleanup import LetToConst
10
+ from .transforms.cleanup import OptionalCatchBinding
11
+ from .transforms.cleanup import ReturnUndefinedCleanup
12
+ from .transforms.cleanup import TrailingReturnRemover
13
+ from .transforms.cleanup import VarToConst
8
14
  from .transforms.constant_prop import ConstantProp
9
15
  from .transforms.control_flow import ControlFlowRecoverer
10
16
  from .transforms.dead_branch import DeadBranchRemover
17
+ from .transforms.dead_class_props import DeadClassPropRemover
18
+ from .transforms.dead_expressions import DeadExpressionRemover
19
+ from .transforms.dead_object_props import DeadObjectPropRemover
20
+ from .transforms.else_if_flatten import ElseIfFlattener
21
+ from .transforms.enum_resolver import EnumResolver
11
22
  from .transforms.eval_unpack import eval_unpack
12
23
  from .transforms.eval_unpack import is_eval_packed
13
24
  from .transforms.expression_simplifier import ExpressionSimplifier
25
+ from .transforms.global_alias import GlobalAliasInliner
14
26
  from .transforms.hex_escapes import HexEscapes
15
27
  from .transforms.hex_escapes import decode_hex_escapes_source
28
+ from .transforms.hex_numerics import HexNumerics
29
+ from .transforms.aa_decode import aa_decode
30
+ from .transforms.aa_decode import is_aa_encoded
16
31
  from .transforms.jj_decode import is_jj_encoded
17
32
  from .transforms.jj_decode import jj_decode
18
- from .transforms.jj_decode import jj_decode_via_eval
19
33
  from .transforms.jsfuck_decode import is_jsfuck
20
34
  from .transforms.jsfuck_decode import jsfuck_decode
21
35
  from .transforms.logical_to_if import LogicalToIf
36
+ from .transforms.member_chain_resolver import MemberChainResolver
37
+ from .transforms.noop_calls import NoopCallRemover
38
+ from .transforms.nullish_coalescing import NullishCoalescing
22
39
  from .transforms.object_packer import ObjectPacker
23
40
  from .transforms.object_simplifier import ObjectSimplifier
41
+ from .transforms.optional_chaining import OptionalChaining
24
42
  from .transforms.property_simplifier import PropertySimplifier
25
43
  from .transforms.proxy_functions import ProxyFunctionInliner
26
44
  from .transforms.reassignment import ReassignmentRemover
45
+ from .transforms.require_inliner import RequireInliner
27
46
  from .transforms.sequence_splitter import SequenceSplitter
47
+ from .transforms.single_use_vars import SingleUseVarInliner
28
48
  from .transforms.string_revealer import StringRevealer
49
+ from .transforms.unreachable_code import UnreachableCodeRemover
29
50
  from .transforms.unused_vars import UnusedVariableRemover
51
+ from .transforms.variable_renamer import VariableRenamer
52
+ from .transforms.xor_string_decode import XorStringDecoder
30
53
  from .traverser import simple_traverse
31
54
 
32
55
 
@@ -36,15 +59,36 @@ from .traverser import simple_traverse
36
59
  TRANSFORM_CLASSES = [
37
60
  StringRevealer,
38
61
  HexEscapes,
62
+ HexNumerics,
63
+ ClassStringDecoder,
64
+ XorStringDecoder,
65
+ MemberChainResolver,
66
+ DeadClassPropRemover,
67
+ ClassStaticResolver,
68
+ EnumResolver,
69
+ RequireInliner,
70
+ GlobalAliasInliner,
39
71
  UnusedVariableRemover,
40
72
  ConstantProp,
41
73
  ReassignmentRemover,
74
+ SingleUseVarInliner,
42
75
  DeadBranchRemover,
76
+ UnreachableCodeRemover,
77
+ NoopCallRemover,
78
+ EmptyIfRemover,
79
+ DeadObjectPropRemover,
43
80
  ObjectPacker,
44
81
  ProxyFunctionInliner,
45
82
  SequenceSplitter,
83
+ DeadExpressionRemover,
46
84
  ExpressionSimplifier,
85
+ NullishCoalescing,
86
+ OptionalChaining,
47
87
  LogicalToIf,
88
+ ElseIfFlattener,
89
+ OptionalCatchBinding,
90
+ ReturnUndefinedCleanup,
91
+ TrailingReturnRemover,
48
92
  ControlFlowRecoverer,
49
93
  PropertySimplifier,
50
94
  AntiTamperRemover,
@@ -87,7 +131,7 @@ class Deobfuscator:
87
131
  Returns decoded code if an encoding/packing was detected and decoded,
88
132
  or None to continue with the normal AST pipeline.
89
133
  """
90
- # JSFUCK check (must be first — these are whole-file encodings)
134
+ # JSFuck check (must be first — these are whole-file encodings)
91
135
  if is_jsfuck(code):
92
136
  decoded = jsfuck_decode(code)
93
137
  if decoded:
@@ -101,7 +145,7 @@ class Deobfuscator:
101
145
 
102
146
  # JJEncode check
103
147
  if is_jj_encoded(code):
104
- decoded = jj_decode(code) or jj_decode_via_eval(code)
148
+ decoded = jj_decode(code)
105
149
  if decoded:
106
150
  return decoded
107
151
 
@@ -113,6 +157,9 @@ class Deobfuscator:
113
157
 
114
158
  return None
115
159
 
160
+ # Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
161
+ _MAX_OUTER_CYCLES = 5
162
+
116
163
  def execute(self):
117
164
  """Run all transforms and return cleaned source."""
118
165
  code = self.original_code
@@ -134,25 +181,79 @@ class Deobfuscator:
134
181
  return decoded
135
182
  return self.original_code
136
183
 
137
- # Determine optimization mode based on code size
138
- code_size = len(code)
184
+ # Outer loop: run AST transforms until generate→re-parse converges.
185
+ # Post-passes (VariableRenamer, VarToConst, LetToConst) only run on
186
+ # the final cycle to avoid interfering with subsequent transform rounds.
187
+ previous_code = code
188
+ last_changed_ast = None
189
+ try:
190
+ for _cycle in range(self._MAX_OUTER_CYCLES):
191
+ changed = self._run_ast_transforms(
192
+ ast,
193
+ code_size=len(previous_code),
194
+ )
195
+
196
+ if not changed:
197
+ break
198
+
199
+ last_changed_ast = ast
200
+
201
+ try:
202
+ generated = generate(ast)
203
+ except Exception:
204
+ break
205
+
206
+ if generated == previous_code:
207
+ break
208
+
209
+ previous_code = generated
210
+
211
+ # Re-parse for the next cycle
212
+ try:
213
+ ast = parse(generated)
214
+ except SyntaxError:
215
+ break
216
+
217
+ # Run post-passes on the final AST (always — they're cheap and handle
218
+ # cosmetic transforms like var→const even when no main transforms fired)
219
+ any_post_changed = False
220
+ for post_transform in [VariableRenamer, VarToConst, LetToConst]:
221
+ try:
222
+ if post_transform(ast).execute():
223
+ any_post_changed = True
224
+ except Exception:
225
+ pass
226
+
227
+ if last_changed_ast is None and not any_post_changed:
228
+ return self.original_code
229
+
230
+ try:
231
+ return generate(ast)
232
+ except Exception:
233
+ return previous_code
234
+ except RecursionError:
235
+ # Safety net: esprima's parser is purely recursive with no depth
236
+ # limit, so deeply nested JS hits Python's recursion limit during
237
+ # parsing or re-parsing. Our AST walkers are cheaper per level
238
+ # but also recursive. Return best result so far.
239
+ return previous_code
240
+
241
+ def _run_ast_transforms(self, ast, code_size=0):
242
+ """Run all AST transform passes. Returns True if any transform changed the AST."""
243
+ node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
244
+
139
245
  lite_mode = code_size > _MAX_CODE_SIZE
140
246
  max_iterations = self.max_iterations
141
247
  if code_size > _LARGE_FILE_SIZE:
142
248
  max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
143
249
 
144
- # Check node count for expensive transform gating
145
- node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
146
-
147
250
  # For very large ASTs, further reduce iterations
148
251
  if node_count > 100_000:
149
252
  max_iterations = min(max_iterations, 3)
150
253
 
151
254
  # Build transform list based on mode
152
255
  transform_classes = TRANSFORM_CLASSES
153
- if lite_mode:
154
- transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
155
- elif node_count > _NODE_COUNT_LIMIT:
256
+ if lite_mode or node_count > _NODE_COUNT_LIMIT:
156
257
  transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
157
258
 
158
259
  # Track which transforms are no longer productive
@@ -182,10 +283,4 @@ class Deobfuscator:
182
283
  if not modified:
183
284
  break
184
285
 
185
- if not any_transform_changed:
186
- return self.original_code
187
-
188
- try:
189
- return generate(ast)
190
- except Exception:
191
- return self.original_code
286
+ return any_transform_changed
@@ -363,9 +363,12 @@ def _gen_member(node, indent):
363
363
  object_code = f'({object_code})'
364
364
 
365
365
  property_code = generate(node['property'], indent)
366
+ dot = '?.' if node.get('optional') else '.'
366
367
  if computed:
368
+ if node.get('optional'):
369
+ return f'{object_code}?.[{property_code}]'
367
370
  return f'{object_code}[{property_code}]'
368
- return f'{object_code}.{property_code}'
371
+ return f'{object_code}{dot}{property_code}'
369
372
 
370
373
 
371
374
  def _gen_call(node, indent):
@@ -374,6 +377,8 @@ def _gen_call(node, indent):
374
377
  if callee_type in ('FunctionExpression', 'ArrowFunctionExpression', 'SequenceExpression'):
375
378
  callee = f'({callee})'
376
379
  args = ', '.join(generate(a, indent) for a in node.get('arguments', []))
380
+ if node.get('optional'):
381
+ return f'{callee}?.({args})'
377
382
  return f'{callee}({args})'
378
383
 
379
384