pyjsclear 0.1.0__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.
Files changed (38) hide show
  1. pyjsclear/__init__.py +47 -0
  2. pyjsclear/__main__.py +37 -0
  3. pyjsclear/deobfuscator.py +191 -0
  4. pyjsclear/generator.py +772 -0
  5. pyjsclear/parser.py +46 -0
  6. pyjsclear/scope.py +283 -0
  7. pyjsclear/transforms/__init__.py +0 -0
  8. pyjsclear/transforms/aa_decode.py +83 -0
  9. pyjsclear/transforms/anti_tamper.py +106 -0
  10. pyjsclear/transforms/base.py +24 -0
  11. pyjsclear/transforms/constant_prop.py +103 -0
  12. pyjsclear/transforms/control_flow.py +330 -0
  13. pyjsclear/transforms/dead_branch.py +71 -0
  14. pyjsclear/transforms/eval_unpack.py +133 -0
  15. pyjsclear/transforms/expression_simplifier.py +403 -0
  16. pyjsclear/transforms/hex_escapes.py +68 -0
  17. pyjsclear/transforms/jj_decode.py +91 -0
  18. pyjsclear/transforms/jsfuck_decode.py +93 -0
  19. pyjsclear/transforms/logical_to_if.py +173 -0
  20. pyjsclear/transforms/object_packer.py +133 -0
  21. pyjsclear/transforms/object_simplifier.py +192 -0
  22. pyjsclear/transforms/property_simplifier.py +43 -0
  23. pyjsclear/transforms/proxy_functions.py +193 -0
  24. pyjsclear/transforms/reassignment.py +183 -0
  25. pyjsclear/transforms/sequence_splitter.py +215 -0
  26. pyjsclear/transforms/string_revealer.py +1259 -0
  27. pyjsclear/transforms/unused_vars.py +111 -0
  28. pyjsclear/traverser.py +195 -0
  29. pyjsclear/utils/__init__.py +0 -0
  30. pyjsclear/utils/ast_helpers.py +257 -0
  31. pyjsclear/utils/string_decoders.py +141 -0
  32. pyjsclear-0.1.0.dist-info/METADATA +168 -0
  33. pyjsclear-0.1.0.dist-info/RECORD +38 -0
  34. pyjsclear-0.1.0.dist-info/WHEEL +5 -0
  35. pyjsclear-0.1.0.dist-info/entry_points.txt +2 -0
  36. pyjsclear-0.1.0.dist-info/licenses/LICENSE +201 -0
  37. pyjsclear-0.1.0.dist-info/licenses/NOTICE +19 -0
  38. pyjsclear-0.1.0.dist-info/top_level.txt +1 -0
pyjsclear/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Pure Python JavaScript deobfuscation library.
2
+
3
+ Combines functionality from obfuscator-io-deobfuscator (13 AST transforms)
4
+ and javascript-deobfuscator (3 surface-cleanup modules) into a single
5
+ Python package with no Node.js dependency.
6
+ """
7
+
8
+ from .deobfuscator import Deobfuscator
9
+
10
+
11
+ __version__ = '0.1.0'
12
+
13
+
14
+ def deobfuscate(code, max_iterations=50):
15
+ """Deobfuscate JavaScript code. Returns cleaned source.
16
+
17
+ Args:
18
+ code: JavaScript source code string.
19
+ max_iterations: Maximum transform passes (default 50).
20
+
21
+ Returns:
22
+ Deobfuscated JavaScript source code.
23
+ """
24
+ return Deobfuscator(code, max_iterations=max_iterations).execute()
25
+
26
+
27
+ def deobfuscate_file(input_path, output_path=None, max_iterations=50):
28
+ """Deobfuscate a JavaScript file.
29
+
30
+ Args:
31
+ input_path: Path to input JS file.
32
+ output_path: Path to write output (if None, returns string).
33
+ max_iterations: Maximum transform passes.
34
+
35
+ Returns:
36
+ True if content changed (when output_path given), or the deobfuscated string.
37
+ """
38
+ with open(input_path, 'r', errors='replace') as f:
39
+ code = f.read()
40
+
41
+ result = deobfuscate(code, max_iterations=max_iterations)
42
+
43
+ if output_path:
44
+ with open(output_path, 'w') as f:
45
+ f.write(result)
46
+ return result != code
47
+ return result
pyjsclear/__main__.py ADDED
@@ -0,0 +1,37 @@
1
+ """CLI entry point: python -m pyjsclear input.js [-o output.js]"""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from . import deobfuscate
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description='Deobfuscate JavaScript files.')
11
+ parser.add_argument('input', help='Input JS file (use - for stdin)')
12
+ parser.add_argument('-o', '--output', help='Output file (default: stdout)')
13
+ parser.add_argument(
14
+ '--max-iterations',
15
+ type=int,
16
+ default=50,
17
+ help='Maximum transform passes (default: 50)',
18
+ )
19
+ args = parser.parse_args()
20
+
21
+ if args.input == '-':
22
+ code = sys.stdin.read()
23
+ else:
24
+ with open(args.input, 'r', errors='replace') as input_file:
25
+ code = input_file.read()
26
+
27
+ result = deobfuscate(code, max_iterations=args.max_iterations)
28
+
29
+ if args.output:
30
+ with open(args.output, 'w') as output_file:
31
+ output_file.write(result)
32
+ else:
33
+ sys.stdout.write(result)
34
+
35
+
36
+ if __name__ == '__main__':
37
+ main()
@@ -0,0 +1,191 @@
1
+ """Multi-pass deobfuscation orchestrator."""
2
+
3
+ from .generator import generate
4
+ from .parser import parse
5
+ from .transforms.aa_decode import aa_decode
6
+ from .transforms.aa_decode import is_aa_encoded
7
+ from .transforms.anti_tamper import AntiTamperRemover
8
+ from .transforms.constant_prop import ConstantProp
9
+ from .transforms.control_flow import ControlFlowRecoverer
10
+ from .transforms.dead_branch import DeadBranchRemover
11
+ from .transforms.eval_unpack import eval_unpack
12
+ from .transforms.eval_unpack import is_eval_packed
13
+ from .transforms.expression_simplifier import ExpressionSimplifier
14
+ from .transforms.hex_escapes import HexEscapes
15
+ from .transforms.hex_escapes import decode_hex_escapes_source
16
+ from .transforms.jj_decode import is_jj_encoded
17
+ from .transforms.jj_decode import jj_decode
18
+ from .transforms.jj_decode import jj_decode_via_eval
19
+ from .transforms.jsfuck_decode import is_jsfuck
20
+ from .transforms.jsfuck_decode import jsfuck_decode
21
+ from .transforms.logical_to_if import LogicalToIf
22
+ from .transforms.object_packer import ObjectPacker
23
+ from .transforms.object_simplifier import ObjectSimplifier
24
+ from .transforms.property_simplifier import PropertySimplifier
25
+ from .transforms.proxy_functions import ProxyFunctionInliner
26
+ from .transforms.reassignment import ReassignmentRemover
27
+ from .transforms.sequence_splitter import SequenceSplitter
28
+ from .transforms.string_revealer import StringRevealer
29
+ from .transforms.unused_vars import UnusedVariableRemover
30
+ from .traverser import simple_traverse
31
+
32
+
33
+ # StringRevealer runs first to handle string arrays before other transforms
34
+ # modify the wrapper function structure.
35
+ # Remaining transforms follow obfuscator-io-deobfuscator order.
36
+ TRANSFORM_CLASSES = [
37
+ StringRevealer,
38
+ HexEscapes,
39
+ UnusedVariableRemover,
40
+ ConstantProp,
41
+ ReassignmentRemover,
42
+ DeadBranchRemover,
43
+ ObjectPacker,
44
+ ProxyFunctionInliner,
45
+ SequenceSplitter,
46
+ ExpressionSimplifier,
47
+ LogicalToIf,
48
+ ControlFlowRecoverer,
49
+ PropertySimplifier,
50
+ AntiTamperRemover,
51
+ ObjectSimplifier,
52
+ StringRevealer,
53
+ ]
54
+
55
+ # Expensive transforms to skip in lite mode (large files)
56
+ _EXPENSIVE_TRANSFORMS = {ControlFlowRecoverer, ProxyFunctionInliner, ObjectPacker}
57
+
58
+ # Large file thresholds
59
+ _LARGE_FILE_SIZE = 500_000 # 500KB - reduce iterations
60
+ _MAX_CODE_SIZE = 2_000_000 # 2MB - use lite mode
61
+ _LITE_MAX_ITERATIONS = 10
62
+ _NODE_COUNT_LIMIT = 50_000 # Skip ControlFlowRecoverer above this
63
+
64
+
65
+ def _count_nodes(ast):
66
+ """Count total AST nodes."""
67
+ count = 0
68
+
69
+ def cb(node, parent):
70
+ nonlocal count
71
+ count += 1
72
+
73
+ simple_traverse(ast, cb)
74
+ return count
75
+
76
+
77
+ class Deobfuscator:
78
+ """Multi-pass JavaScript deobfuscator."""
79
+
80
+ def __init__(self, code, max_iterations=50):
81
+ self.original_code = code
82
+ self.max_iterations = max_iterations
83
+
84
+ def _run_pre_passes(self, code):
85
+ """Run encoding detection and eval unpacking pre-passes.
86
+
87
+ Returns decoded code if an encoding/packing was detected and decoded,
88
+ or None to continue with the normal AST pipeline.
89
+ """
90
+ # JSFUCK check (must be first — these are whole-file encodings)
91
+ if is_jsfuck(code):
92
+ decoded = jsfuck_decode(code)
93
+ if decoded:
94
+ return decoded
95
+
96
+ # AAEncode check
97
+ if is_aa_encoded(code):
98
+ decoded = aa_decode(code)
99
+ if decoded:
100
+ return decoded
101
+
102
+ # JJEncode check
103
+ if is_jj_encoded(code):
104
+ decoded = jj_decode(code) or jj_decode_via_eval(code)
105
+ if decoded:
106
+ return decoded
107
+
108
+ # Eval packer check
109
+ if is_eval_packed(code):
110
+ decoded = eval_unpack(code)
111
+ if decoded:
112
+ return decoded
113
+
114
+ return None
115
+
116
+ def execute(self):
117
+ """Run all transforms and return cleaned source."""
118
+ code = self.original_code
119
+
120
+ # Pre-pass: encoding detection and eval unpacking
121
+ decoded = self._run_pre_passes(code)
122
+ if decoded:
123
+ # Feed decoded result back through the full pipeline for further cleanup
124
+ sub = Deobfuscator(decoded, max_iterations=self.max_iterations)
125
+ return sub.execute()
126
+
127
+ # Try to parse; if it fails, apply source-level hex decoding as fallback
128
+ try:
129
+ ast = parse(code)
130
+ except SyntaxError:
131
+ # Source-level hex decode for unparseable files (e.g. ES modules)
132
+ decoded = decode_hex_escapes_source(code)
133
+ if decoded != code:
134
+ return decoded
135
+ return self.original_code
136
+
137
+ # Determine optimization mode based on code size
138
+ code_size = len(code)
139
+ lite_mode = code_size > _MAX_CODE_SIZE
140
+ max_iterations = self.max_iterations
141
+ if code_size > _LARGE_FILE_SIZE:
142
+ max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
143
+
144
+ # Check node count for expensive transform gating
145
+ node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
146
+
147
+ # For very large ASTs, further reduce iterations
148
+ if node_count > 100_000:
149
+ max_iterations = min(max_iterations, 3)
150
+
151
+ # Build transform list based on mode
152
+ 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:
156
+ transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
157
+
158
+ # Track which transforms are no longer productive
159
+ skip_transforms = set()
160
+
161
+ # Multi-pass transform loop
162
+ any_transform_changed = False
163
+ for i in range(max_iterations):
164
+ modified = False
165
+ for transform_class in transform_classes:
166
+ if transform_class in skip_transforms:
167
+ continue
168
+ try:
169
+ transform = transform_class(ast)
170
+ result = transform.execute()
171
+ except Exception:
172
+ continue
173
+ if result:
174
+ modified = True
175
+ any_transform_changed = True
176
+ else:
177
+ # If a transform didn't change anything after the first pass,
178
+ # skip it in subsequent iterations
179
+ if i > 0:
180
+ skip_transforms.add(transform_class)
181
+
182
+ if not modified:
183
+ break
184
+
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