text-deobfuscate 0.1.0a1__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.
- text_deobfuscate-0.1.0a1/LICENSE +21 -0
- text_deobfuscate-0.1.0a1/PKG-INFO +257 -0
- text_deobfuscate-0.1.0a1/README.md +227 -0
- text_deobfuscate-0.1.0a1/pyproject.toml +52 -0
- text_deobfuscate-0.1.0a1/pyproject.toml.orig +41 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/__init__.py +13 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/__main__.py +5 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/cli.py +30 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/core.py +21 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/NOTICE.md +10 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/SCOWL-COPYRIGHT.txt +248 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/lookalikes.txt +597 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/ngrams.txt +33813 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/scripts.txt +999 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/data/words.txt +92459 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/engine.py +196 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/protect.py +42 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/py.typed +0 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/resources.py +39 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/scorer.py +90 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/scripts.py +23 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/__init__.py +57 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/base.py +52 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/base64.py +61 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/bytes.py +58 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/caesar.py +203 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/emoji.py +59 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/escapes.py +121 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/invisible.py +70 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/leet.py +415 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/lookalike.py +298 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/mail.py +107 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/marks.py +87 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/misspelling.py +113 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/morse.py +87 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/repeated.py +73 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/reverse.py +127 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/runs.py +55 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/separator.py +145 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/shorthand.py +99 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/transforms/styled.py +90 -0
- text_deobfuscate-0.1.0a1/src/text_deobfuscate/types.py +33 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lapid Harel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: text-deobfuscate
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Undo text obfuscation (encodings and visual tricks) and report which transforms were applied.
|
|
5
|
+
Keywords: deobfuscation,text,leetspeak,homoglyph,phishing,unicode,normalization,security
|
|
6
|
+
Author: Lapid Harel
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Classifier: Topic :: Text Processing
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Project-URL: Homepage, https://github.com/lapid/text-deobfuscate
|
|
25
|
+
Project-URL: Repository, https://github.com/lapid/text-deobfuscate
|
|
26
|
+
Project-URL: Issues, https://github.com/lapid/text-deobfuscate/issues
|
|
27
|
+
Project-URL: Changelog, https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md
|
|
28
|
+
Project-URL: Documentation, https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# text-deobfuscate
|
|
32
|
+
|
|
33
|
+
Takes a text and returns the deobfuscated text, plus a record of which transforms were applied
|
|
34
|
+
and where.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
>>> from text_deobfuscate import deobfuscate
|
|
38
|
+
>>> deobfuscate("Please confirm your account").text
|
|
39
|
+
'Please confirm your account'
|
|
40
|
+
>>> deobfuscate("Cl1ck h3r3 to cla1m your pr1ze", strictness="balanced").text
|
|
41
|
+
'Click here to claim your prize'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Status: version 0.1.0a1, an alpha.** The library and the command work,
|
|
45
|
+
thirty-two transforms are implemented and measured, and the numbers below say how far to trust
|
|
46
|
+
them: they are mostly from samples our own generators produced, so read "How well it works"
|
|
47
|
+
before relying on it.
|
|
48
|
+
|
|
49
|
+
## Installing
|
|
50
|
+
|
|
51
|
+
From PyPI. The version is an alpha, so pip needs `--pre`:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pip install --pre text-deobfuscate # or: uv add --prerelease allow text-deobfuscate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or from a clone, to work on it:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
git clone https://github.com/lapid/text-deobfuscate && cd text-deobfuscate
|
|
61
|
+
uv sync # or: pip install .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It needs **Python 3.10 or newer** and has **no dependencies**.
|
|
65
|
+
|
|
66
|
+
## Using it
|
|
67
|
+
|
|
68
|
+
As a library. `deobfuscate()` returns the text and an ordered list of steps; each step says what
|
|
69
|
+
changed, where it sat in the text as it stood before that step, and how confident the engine was:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from text_deobfuscate import deobfuscate
|
|
73
|
+
|
|
74
|
+
result = deobfuscate("Please confirm your account")
|
|
75
|
+
result.text # 'Please confirm your account'
|
|
76
|
+
[step.transform for step in result.steps] # ['zero-width', 'html-entity']
|
|
77
|
+
result.steps[0].start, result.steps[0].end # 25, 33
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
As a command, with `--json` for the whole record:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
$ text-deobfuscate "UGxlYXNlIHNlbmQgdGhlIHJlcG9ydCBiZWZvcmUgRnJpZGF5"
|
|
84
|
+
Please send the report before Friday
|
|
85
|
+
|
|
86
|
+
$ text-deobfuscate --strictness balanced "c o n f i r m y o u r a c c o u n t"
|
|
87
|
+
confirm your account
|
|
88
|
+
|
|
89
|
+
$ text-deobfuscate --json "Thx 4 ur msg" # text plus steps, as JSON
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It reads standard input when given no argument, so it pipes. Piped or redirected, it writes the
|
|
93
|
+
text and nothing else — no trailing newline is added, so `text-deobfuscate x > file` holds exactly the
|
|
94
|
+
text. A terminal gets a line ending, so the prompt does not land mid-sentence.
|
|
95
|
+
|
|
96
|
+
### What is public
|
|
97
|
+
|
|
98
|
+
`deobfuscate`, `Result`, `Step`, `STRICTNESS_LEVELS` and `__version__`, and the `text-deobfuscate`
|
|
99
|
+
command. Everything else in the package (`engine`, `scorer`, `transforms`, `protect`, `scripts` and
|
|
100
|
+
the rest) is internal and may change in any release, even though Python lets you import it. The
|
|
101
|
+
package ships type information (`py.typed`); the public names check cleanly with mypy, and the
|
|
102
|
+
internals still have some warnings.
|
|
103
|
+
|
|
104
|
+
## Strictness
|
|
105
|
+
|
|
106
|
+
Three levels, because the tricks differ in how certain they are. `conservative` is the default.
|
|
107
|
+
|
|
108
|
+
| Level | Adds | Example |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `conservative` | Encodings and escapes, invisible characters, styled and fullwidth letters, ciphers, reversal, Morse — things ordinary English does not contain | `UGxlYXNl…`, `e`, `\141\142`, `ⓒⓞⓝⓕⓘⓡⓜ` |
|
|
111
|
+
| `balanced` | Ambiguous visual tricks: lookalike letters, added accents, overlaid marks, leetspeak, inserted separators | `раssword` with Cyrillic letters, `Cl1ck`, `c o n f i r m` |
|
|
112
|
+
| `aggressive` | A lower acceptance margin, plus informal writing from short lists: stretched words, texting shorthand, common misspellings, emoji for a word | `sooooo`, `l8r`, `recieve`, `📞 me` |
|
|
113
|
+
|
|
114
|
+
Below `aggressive`, informal writing comes back exactly as written.
|
|
115
|
+
|
|
116
|
+
## How it decides
|
|
117
|
+
|
|
118
|
+
**Changing clean text is treated as the worst mistake the tool can make.** Transforms only
|
|
119
|
+
*propose* rewrites; the engine applies one only when the result scores as more plausible English
|
|
120
|
+
than what it replaces, judged by a word list and a character four-gram table. Layered
|
|
121
|
+
obfuscation is decoded first and judged by what it finally turns into. URLs, paths, hashes and
|
|
122
|
+
other things that must not be touched are shielded. Nothing is ever executed — only decoded,
|
|
123
|
+
within size and depth limits.
|
|
124
|
+
|
|
125
|
+
Details in [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md) and [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md).
|
|
126
|
+
|
|
127
|
+
## How well it works
|
|
128
|
+
|
|
129
|
+
**Read this first.** Most samples were made by our own obfuscation generators, so they test the
|
|
130
|
+
decoders against the people who wrote them. The independent evidence is smaller: 3,000 real
|
|
131
|
+
phishing sentences, 25 sentences disguised by hand, and sets made by someone else's generator
|
|
132
|
+
(below). The development sets have been run and tuned against many times, and the test split was
|
|
133
|
+
used up in October 2026. Treat every figure as an upper bound for text you have not seen.
|
|
134
|
+
|
|
135
|
+
Recovery means the text came back exactly, ignoring capital letters; clean texts changed is
|
|
136
|
+
measured strictly. The tool is judged first on **not changing clean text**.
|
|
137
|
+
|
|
138
|
+
**Development sets, thirty-two transforms (2026-10-29):** 7,850 clean texts, about 9,500
|
|
139
|
+
obfuscated ones, and the 3,000 real phishing sentences (kept outside the repository).
|
|
140
|
+
|
|
141
|
+
| Level | Clean texts changed | Recovered | Made worse | Real phishing | Hand-disguised |
|
|
142
|
+
|---|---|---|---|---|---|
|
|
143
|
+
| `conservative` | 0.01% (1 of 7,850) | 89.3% (5,273 of 5,905) | 1.2% | — | — |
|
|
144
|
+
| `balanced` | 0.01% (1 of 7,850) | 88.1% (10,292 of 11,680) | 0.6% | 90.8% | 15 of 25 |
|
|
145
|
+
| `aggressive` | 0.28% (22 of 7,850) | 86.8% (10,864 of 12,520) | 0.7% | 94.4% | 17 of 25 |
|
|
146
|
+
|
|
147
|
+
Each level is scored on the samples it is meant to undo, so the denominators differ and the rows
|
|
148
|
+
are not comparable with each other. Adding transforms or harder samples lowers a total without
|
|
149
|
+
anything getting worse; judge a trick by its own row in
|
|
150
|
+
[`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
|
|
151
|
+
|
|
152
|
+
**Sets made by someone else's generator, scored at `balanced`.** The first is 6,000 rows kept
|
|
153
|
+
outside the repository; we tuned against it, so it is optimistic. The second is 3,000 fresh rows
|
|
154
|
+
from unused text of the same source and generator: new rows, but the same style of obfuscation.
|
|
155
|
+
Neither tests a different author's habits.
|
|
156
|
+
|
|
157
|
+
| Trick | Tuned set | Fresh rows |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| base64, ROT13, zero-width, spacing | 99–100% | 100% |
|
|
160
|
+
| fullwidth, reversal | 96–99% | 98.5–99.6% |
|
|
161
|
+
| homoglyph | 96.5% | 97.8% |
|
|
162
|
+
| a symbol between every character | 99.8% | 99.3% |
|
|
163
|
+
| leetspeak | 59.7% | 57.5% |
|
|
164
|
+
| alternating capitals | 0% (99.8% ignoring capitals) | 0% (100%) |
|
|
165
|
+
| dropped vowels | 0% | 0% |
|
|
166
|
+
| **all rows** | **77.3%** | **77.5%** |
|
|
167
|
+
|
|
168
|
+
Alternating capitals come back in lower case: the tool does not restore capitals. Dropped
|
|
169
|
+
vowels are informal writing, out of scope by design.
|
|
170
|
+
|
|
171
|
+
**Where it does worst** (development sets, `balanced`): a disguised sentence inside plain text is
|
|
172
|
+
the weakest case. Reverse in context recovers 2.5%, ROT47 15%, per-word reversal 17.5%, Atbash
|
|
173
|
+
45%, Caesar 58%, ROT13 70%. Leetspeak with every letter replaced recovers 66% of whole texts;
|
|
174
|
+
accent substitution 62–71%. Encodings that fill a whole text (base64, hex, base32, escapes) are
|
|
175
|
+
above 90%.
|
|
176
|
+
|
|
177
|
+
**Time:** the median text takes about 2 ms and the 95th percentile about 15 ms. The slowest
|
|
178
|
+
inputs are long and hostile: 100,000 random printable characters take about ten seconds at
|
|
179
|
+
`aggressive` (six at `balanced`), and everything else tested at that size takes under 5. One call
|
|
180
|
+
scans at most one million characters in all.
|
|
181
|
+
|
|
182
|
+
**The test split** was run once, on 2026-10-12, for twenty-three transforms. It showed clean
|
|
183
|
+
texts changed at 3, 13 and 38 of 7,442 for the three levels. Reading the 13 at `balanced` on
|
|
184
|
+
2026-10-27 led to fixes for leetspeak on names and numbers (`P-51's`, `S4C`, `tty1`), and 5
|
|
185
|
+
remain, so the split no longer measures anything independently.
|
|
186
|
+
|
|
187
|
+
## Known limits
|
|
188
|
+
|
|
189
|
+
Honest list; each is measured and logged.
|
|
190
|
+
|
|
191
|
+
- **Clean texts changed was higher on test than on dev** (13 against 1 at `balanced`). Reading the
|
|
192
|
+
13 showed leetspeak applied to names and numbers; that is fixed, and 5 remain, which are
|
|
193
|
+
arguably correct changes. The unseen text of a real user may still be changed in ways our
|
|
194
|
+
samples do not show: report it.
|
|
195
|
+
- **Hand-disguised sentences did not carry over** from dev (15 of 25) to test (5 of 22). Human
|
|
196
|
+
disguises vary more than generated ones.
|
|
197
|
+
- **Most of the evidence comes from generators we wrote or tuned against.** See the top of
|
|
198
|
+
"How well it works".
|
|
199
|
+
- **Leetspeak** is about 58% exact on someone else's generator. Lone `1` and `4` in lightly
|
|
200
|
+
disguised text need a second disguised word nearby, and the wider table (`8`, `9`, `2`) is weak.
|
|
201
|
+
- **Time and size:** inputs over 100,000 characters are returned unchanged, and one call stops
|
|
202
|
+
scanning after a fixed budget, so a very long text may be only partly decoded. The slowest
|
|
203
|
+
input found, 100,000 random printable characters, takes about ten seconds at `aggressive` and
|
|
204
|
+
six at `balanced`; 30,000 take about a second. If you pass untrusted text to it in a service,
|
|
205
|
+
cap its length (a few thousand characters covers messages and emails) and use a timeout.
|
|
206
|
+
- **A text reversed word by word cannot be told from a wholly reversed one** unless a full stop
|
|
207
|
+
settles it, so 51 of 210 such samples come back with the words in the wrong order. 44 of those
|
|
208
|
+
are still perfectly legible; this is a limit of the ambiguity, not a bug to fix.
|
|
209
|
+
- **A lone disguised word** with everyday accents, or a lone Greek "a", is left alone at
|
|
210
|
+
`balanced`: one word is not enough evidence.
|
|
211
|
+
- **A capital hidden behind a leetspeak digit is lost** (`Cl1ck` → `click`). Recovery is measured
|
|
212
|
+
ignoring capitals for this reason.
|
|
213
|
+
- **Shifted or reversed words inside otherwise clean text** recover poorly.
|
|
214
|
+
- **Text in another language** passes through unchanged in nearly every case: 75 short phrases in
|
|
215
|
+
six languages come back untouched at every level. A short phrase used to be reversed ("No pasa
|
|
216
|
+
nada" became "adan asap oN"); texts of fewer than four words and sixteen letters are no longer
|
|
217
|
+
reversed as a whole, which also means a very short reversed text is not recovered. A longer
|
|
218
|
+
foreign text that reads as English backwards would still be reversed.
|
|
219
|
+
- **At `aggressive`, accents are removed from other words** ("Zoë" may become "Zoe"). Normal English
|
|
220
|
+
spellings with accents (`naïve`, `café`, `résumé`, `jalapeño` and about eighty more) are kept.
|
|
221
|
+
- **No recogniser yet for key-like tokens or code**, so a ticket code in base32 may be decoded.
|
|
222
|
+
- **English only.** Other languages and scripts pass through unchanged, by design.
|
|
223
|
+
- **Deferred to version 2:** bidirectional controls, variation-selector payloads, Punycode, part
|
|
224
|
+
of Unicode's invisible characters, and some styled ranges. Samples for them exist and are
|
|
225
|
+
excluded from a normal run.
|
|
226
|
+
- **Wrong-encoding damage** (`é` for `é`) is out of scope and comes back unchanged.
|
|
227
|
+
- Code obfuscation is out of scope.
|
|
228
|
+
|
|
229
|
+
## Testing and the regression gate
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
uv run python -m unittest discover -s tests # 125 unit tests
|
|
233
|
+
uv run python -m evaluation.gate # the regression gate: pass or fail
|
|
234
|
+
uv run python -m evaluation.run --detail # the full report, per category and per trick
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The gate scores the committed sets at all three levels and fails if clean texts changed rises, if
|
|
238
|
+
any trick recovers fewer samples than before, or if the output stops settling when the tool is run
|
|
239
|
+
over its own output. A deliberate trade-off is made by passing `--update` in the same commit and
|
|
240
|
+
giving the reason in the commit message. More in
|
|
241
|
+
[`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
|
|
242
|
+
|
|
243
|
+
## Documentation
|
|
244
|
+
|
|
245
|
+
- [`docs/taxonomy.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/taxonomy.md): every trick, its level, its main guard, and what is out of scope.
|
|
246
|
+
- [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md): how it works inside, and how to add a transform.
|
|
247
|
+
- [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md): the plausibility scorer and the experiment behind it.
|
|
248
|
+
- [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md): the sets, the measures, and every baseline number.
|
|
249
|
+
|
|
250
|
+
Contributions go through a pull request: the tests and the gate must pass, and a new transform
|
|
251
|
+
comes with samples. The false-change rate on the clean set may not rise.
|
|
252
|
+
|
|
253
|
+
## Licence
|
|
254
|
+
|
|
255
|
+
MIT for our own code and data; see [`LICENSE`](https://github.com/lapid/text-deobfuscate/blob/master/LICENSE). Third-party data keeps its own licence,
|
|
256
|
+
with its source and terms recorded beside it in [`data/`](https://github.com/lapid/text-deobfuscate/tree/master/data/) and
|
|
257
|
+
[`src/text_deobfuscate/data/NOTICE.md`](https://github.com/lapid/text-deobfuscate/blob/master/src/text_deobfuscate/data/NOTICE.md).
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# text-deobfuscate
|
|
2
|
+
|
|
3
|
+
Takes a text and returns the deobfuscated text, plus a record of which transforms were applied
|
|
4
|
+
and where.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
>>> from text_deobfuscate import deobfuscate
|
|
8
|
+
>>> deobfuscate("Please confirm your account").text
|
|
9
|
+
'Please confirm your account'
|
|
10
|
+
>>> deobfuscate("Cl1ck h3r3 to cla1m your pr1ze", strictness="balanced").text
|
|
11
|
+
'Click here to claim your prize'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Status: version 0.1.0a1, an alpha.** The library and the command work,
|
|
15
|
+
thirty-two transforms are implemented and measured, and the numbers below say how far to trust
|
|
16
|
+
them: they are mostly from samples our own generators produced, so read "How well it works"
|
|
17
|
+
before relying on it.
|
|
18
|
+
|
|
19
|
+
## Installing
|
|
20
|
+
|
|
21
|
+
From PyPI. The version is an alpha, so pip needs `--pre`:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install --pre text-deobfuscate # or: uv add --prerelease allow text-deobfuscate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or from a clone, to work on it:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
git clone https://github.com/lapid/text-deobfuscate && cd text-deobfuscate
|
|
31
|
+
uv sync # or: pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It needs **Python 3.10 or newer** and has **no dependencies**.
|
|
35
|
+
|
|
36
|
+
## Using it
|
|
37
|
+
|
|
38
|
+
As a library. `deobfuscate()` returns the text and an ordered list of steps; each step says what
|
|
39
|
+
changed, where it sat in the text as it stood before that step, and how confident the engine was:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from text_deobfuscate import deobfuscate
|
|
43
|
+
|
|
44
|
+
result = deobfuscate("Please confirm your account")
|
|
45
|
+
result.text # 'Please confirm your account'
|
|
46
|
+
[step.transform for step in result.steps] # ['zero-width', 'html-entity']
|
|
47
|
+
result.steps[0].start, result.steps[0].end # 25, 33
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
As a command, with `--json` for the whole record:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
$ text-deobfuscate "UGxlYXNlIHNlbmQgdGhlIHJlcG9ydCBiZWZvcmUgRnJpZGF5"
|
|
54
|
+
Please send the report before Friday
|
|
55
|
+
|
|
56
|
+
$ text-deobfuscate --strictness balanced "c o n f i r m y o u r a c c o u n t"
|
|
57
|
+
confirm your account
|
|
58
|
+
|
|
59
|
+
$ text-deobfuscate --json "Thx 4 ur msg" # text plus steps, as JSON
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It reads standard input when given no argument, so it pipes. Piped or redirected, it writes the
|
|
63
|
+
text and nothing else — no trailing newline is added, so `text-deobfuscate x > file` holds exactly the
|
|
64
|
+
text. A terminal gets a line ending, so the prompt does not land mid-sentence.
|
|
65
|
+
|
|
66
|
+
### What is public
|
|
67
|
+
|
|
68
|
+
`deobfuscate`, `Result`, `Step`, `STRICTNESS_LEVELS` and `__version__`, and the `text-deobfuscate`
|
|
69
|
+
command. Everything else in the package (`engine`, `scorer`, `transforms`, `protect`, `scripts` and
|
|
70
|
+
the rest) is internal and may change in any release, even though Python lets you import it. The
|
|
71
|
+
package ships type information (`py.typed`); the public names check cleanly with mypy, and the
|
|
72
|
+
internals still have some warnings.
|
|
73
|
+
|
|
74
|
+
## Strictness
|
|
75
|
+
|
|
76
|
+
Three levels, because the tricks differ in how certain they are. `conservative` is the default.
|
|
77
|
+
|
|
78
|
+
| Level | Adds | Example |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `conservative` | Encodings and escapes, invisible characters, styled and fullwidth letters, ciphers, reversal, Morse — things ordinary English does not contain | `UGxlYXNl…`, `e`, `\141\142`, `ⓒⓞⓝⓕⓘⓡⓜ` |
|
|
81
|
+
| `balanced` | Ambiguous visual tricks: lookalike letters, added accents, overlaid marks, leetspeak, inserted separators | `раssword` with Cyrillic letters, `Cl1ck`, `c o n f i r m` |
|
|
82
|
+
| `aggressive` | A lower acceptance margin, plus informal writing from short lists: stretched words, texting shorthand, common misspellings, emoji for a word | `sooooo`, `l8r`, `recieve`, `📞 me` |
|
|
83
|
+
|
|
84
|
+
Below `aggressive`, informal writing comes back exactly as written.
|
|
85
|
+
|
|
86
|
+
## How it decides
|
|
87
|
+
|
|
88
|
+
**Changing clean text is treated as the worst mistake the tool can make.** Transforms only
|
|
89
|
+
*propose* rewrites; the engine applies one only when the result scores as more plausible English
|
|
90
|
+
than what it replaces, judged by a word list and a character four-gram table. Layered
|
|
91
|
+
obfuscation is decoded first and judged by what it finally turns into. URLs, paths, hashes and
|
|
92
|
+
other things that must not be touched are shielded. Nothing is ever executed — only decoded,
|
|
93
|
+
within size and depth limits.
|
|
94
|
+
|
|
95
|
+
Details in [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md) and [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md).
|
|
96
|
+
|
|
97
|
+
## How well it works
|
|
98
|
+
|
|
99
|
+
**Read this first.** Most samples were made by our own obfuscation generators, so they test the
|
|
100
|
+
decoders against the people who wrote them. The independent evidence is smaller: 3,000 real
|
|
101
|
+
phishing sentences, 25 sentences disguised by hand, and sets made by someone else's generator
|
|
102
|
+
(below). The development sets have been run and tuned against many times, and the test split was
|
|
103
|
+
used up in October 2026. Treat every figure as an upper bound for text you have not seen.
|
|
104
|
+
|
|
105
|
+
Recovery means the text came back exactly, ignoring capital letters; clean texts changed is
|
|
106
|
+
measured strictly. The tool is judged first on **not changing clean text**.
|
|
107
|
+
|
|
108
|
+
**Development sets, thirty-two transforms (2026-10-29):** 7,850 clean texts, about 9,500
|
|
109
|
+
obfuscated ones, and the 3,000 real phishing sentences (kept outside the repository).
|
|
110
|
+
|
|
111
|
+
| Level | Clean texts changed | Recovered | Made worse | Real phishing | Hand-disguised |
|
|
112
|
+
|---|---|---|---|---|---|
|
|
113
|
+
| `conservative` | 0.01% (1 of 7,850) | 89.3% (5,273 of 5,905) | 1.2% | — | — |
|
|
114
|
+
| `balanced` | 0.01% (1 of 7,850) | 88.1% (10,292 of 11,680) | 0.6% | 90.8% | 15 of 25 |
|
|
115
|
+
| `aggressive` | 0.28% (22 of 7,850) | 86.8% (10,864 of 12,520) | 0.7% | 94.4% | 17 of 25 |
|
|
116
|
+
|
|
117
|
+
Each level is scored on the samples it is meant to undo, so the denominators differ and the rows
|
|
118
|
+
are not comparable with each other. Adding transforms or harder samples lowers a total without
|
|
119
|
+
anything getting worse; judge a trick by its own row in
|
|
120
|
+
[`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
|
|
121
|
+
|
|
122
|
+
**Sets made by someone else's generator, scored at `balanced`.** The first is 6,000 rows kept
|
|
123
|
+
outside the repository; we tuned against it, so it is optimistic. The second is 3,000 fresh rows
|
|
124
|
+
from unused text of the same source and generator: new rows, but the same style of obfuscation.
|
|
125
|
+
Neither tests a different author's habits.
|
|
126
|
+
|
|
127
|
+
| Trick | Tuned set | Fresh rows |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| base64, ROT13, zero-width, spacing | 99–100% | 100% |
|
|
130
|
+
| fullwidth, reversal | 96–99% | 98.5–99.6% |
|
|
131
|
+
| homoglyph | 96.5% | 97.8% |
|
|
132
|
+
| a symbol between every character | 99.8% | 99.3% |
|
|
133
|
+
| leetspeak | 59.7% | 57.5% |
|
|
134
|
+
| alternating capitals | 0% (99.8% ignoring capitals) | 0% (100%) |
|
|
135
|
+
| dropped vowels | 0% | 0% |
|
|
136
|
+
| **all rows** | **77.3%** | **77.5%** |
|
|
137
|
+
|
|
138
|
+
Alternating capitals come back in lower case: the tool does not restore capitals. Dropped
|
|
139
|
+
vowels are informal writing, out of scope by design.
|
|
140
|
+
|
|
141
|
+
**Where it does worst** (development sets, `balanced`): a disguised sentence inside plain text is
|
|
142
|
+
the weakest case. Reverse in context recovers 2.5%, ROT47 15%, per-word reversal 17.5%, Atbash
|
|
143
|
+
45%, Caesar 58%, ROT13 70%. Leetspeak with every letter replaced recovers 66% of whole texts;
|
|
144
|
+
accent substitution 62–71%. Encodings that fill a whole text (base64, hex, base32, escapes) are
|
|
145
|
+
above 90%.
|
|
146
|
+
|
|
147
|
+
**Time:** the median text takes about 2 ms and the 95th percentile about 15 ms. The slowest
|
|
148
|
+
inputs are long and hostile: 100,000 random printable characters take about ten seconds at
|
|
149
|
+
`aggressive` (six at `balanced`), and everything else tested at that size takes under 5. One call
|
|
150
|
+
scans at most one million characters in all.
|
|
151
|
+
|
|
152
|
+
**The test split** was run once, on 2026-10-12, for twenty-three transforms. It showed clean
|
|
153
|
+
texts changed at 3, 13 and 38 of 7,442 for the three levels. Reading the 13 at `balanced` on
|
|
154
|
+
2026-10-27 led to fixes for leetspeak on names and numbers (`P-51's`, `S4C`, `tty1`), and 5
|
|
155
|
+
remain, so the split no longer measures anything independently.
|
|
156
|
+
|
|
157
|
+
## Known limits
|
|
158
|
+
|
|
159
|
+
Honest list; each is measured and logged.
|
|
160
|
+
|
|
161
|
+
- **Clean texts changed was higher on test than on dev** (13 against 1 at `balanced`). Reading the
|
|
162
|
+
13 showed leetspeak applied to names and numbers; that is fixed, and 5 remain, which are
|
|
163
|
+
arguably correct changes. The unseen text of a real user may still be changed in ways our
|
|
164
|
+
samples do not show: report it.
|
|
165
|
+
- **Hand-disguised sentences did not carry over** from dev (15 of 25) to test (5 of 22). Human
|
|
166
|
+
disguises vary more than generated ones.
|
|
167
|
+
- **Most of the evidence comes from generators we wrote or tuned against.** See the top of
|
|
168
|
+
"How well it works".
|
|
169
|
+
- **Leetspeak** is about 58% exact on someone else's generator. Lone `1` and `4` in lightly
|
|
170
|
+
disguised text need a second disguised word nearby, and the wider table (`8`, `9`, `2`) is weak.
|
|
171
|
+
- **Time and size:** inputs over 100,000 characters are returned unchanged, and one call stops
|
|
172
|
+
scanning after a fixed budget, so a very long text may be only partly decoded. The slowest
|
|
173
|
+
input found, 100,000 random printable characters, takes about ten seconds at `aggressive` and
|
|
174
|
+
six at `balanced`; 30,000 take about a second. If you pass untrusted text to it in a service,
|
|
175
|
+
cap its length (a few thousand characters covers messages and emails) and use a timeout.
|
|
176
|
+
- **A text reversed word by word cannot be told from a wholly reversed one** unless a full stop
|
|
177
|
+
settles it, so 51 of 210 such samples come back with the words in the wrong order. 44 of those
|
|
178
|
+
are still perfectly legible; this is a limit of the ambiguity, not a bug to fix.
|
|
179
|
+
- **A lone disguised word** with everyday accents, or a lone Greek "a", is left alone at
|
|
180
|
+
`balanced`: one word is not enough evidence.
|
|
181
|
+
- **A capital hidden behind a leetspeak digit is lost** (`Cl1ck` → `click`). Recovery is measured
|
|
182
|
+
ignoring capitals for this reason.
|
|
183
|
+
- **Shifted or reversed words inside otherwise clean text** recover poorly.
|
|
184
|
+
- **Text in another language** passes through unchanged in nearly every case: 75 short phrases in
|
|
185
|
+
six languages come back untouched at every level. A short phrase used to be reversed ("No pasa
|
|
186
|
+
nada" became "adan asap oN"); texts of fewer than four words and sixteen letters are no longer
|
|
187
|
+
reversed as a whole, which also means a very short reversed text is not recovered. A longer
|
|
188
|
+
foreign text that reads as English backwards would still be reversed.
|
|
189
|
+
- **At `aggressive`, accents are removed from other words** ("Zoë" may become "Zoe"). Normal English
|
|
190
|
+
spellings with accents (`naïve`, `café`, `résumé`, `jalapeño` and about eighty more) are kept.
|
|
191
|
+
- **No recogniser yet for key-like tokens or code**, so a ticket code in base32 may be decoded.
|
|
192
|
+
- **English only.** Other languages and scripts pass through unchanged, by design.
|
|
193
|
+
- **Deferred to version 2:** bidirectional controls, variation-selector payloads, Punycode, part
|
|
194
|
+
of Unicode's invisible characters, and some styled ranges. Samples for them exist and are
|
|
195
|
+
excluded from a normal run.
|
|
196
|
+
- **Wrong-encoding damage** (`é` for `é`) is out of scope and comes back unchanged.
|
|
197
|
+
- Code obfuscation is out of scope.
|
|
198
|
+
|
|
199
|
+
## Testing and the regression gate
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
uv run python -m unittest discover -s tests # 125 unit tests
|
|
203
|
+
uv run python -m evaluation.gate # the regression gate: pass or fail
|
|
204
|
+
uv run python -m evaluation.run --detail # the full report, per category and per trick
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The gate scores the committed sets at all three levels and fails if clean texts changed rises, if
|
|
208
|
+
any trick recovers fewer samples than before, or if the output stops settling when the tool is run
|
|
209
|
+
over its own output. A deliberate trade-off is made by passing `--update` in the same commit and
|
|
210
|
+
giving the reason in the commit message. More in
|
|
211
|
+
[`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md).
|
|
212
|
+
|
|
213
|
+
## Documentation
|
|
214
|
+
|
|
215
|
+
- [`docs/taxonomy.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/taxonomy.md): every trick, its level, its main guard, and what is out of scope.
|
|
216
|
+
- [`docs/architecture.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md): how it works inside, and how to add a transform.
|
|
217
|
+
- [`docs/scorer.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/scorer.md): the plausibility scorer and the experiment behind it.
|
|
218
|
+
- [`docs/evaluation.md`](https://github.com/lapid/text-deobfuscate/blob/master/docs/evaluation.md): the sets, the measures, and every baseline number.
|
|
219
|
+
|
|
220
|
+
Contributions go through a pull request: the tests and the gate must pass, and a new transform
|
|
221
|
+
comes with samples. The false-change rate on the clean set may not rise.
|
|
222
|
+
|
|
223
|
+
## Licence
|
|
224
|
+
|
|
225
|
+
MIT for our own code and data; see [`LICENSE`](https://github.com/lapid/text-deobfuscate/blob/master/LICENSE). Third-party data keeps its own licence,
|
|
226
|
+
with its source and terms recorded beside it in [`data/`](https://github.com/lapid/text-deobfuscate/tree/master/data/) and
|
|
227
|
+
[`src/text_deobfuscate/data/NOTICE.md`](https://github.com/lapid/text-deobfuscate/blob/master/src/text_deobfuscate/data/NOTICE.md).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "text-deobfuscate"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Undo text obfuscation (encodings and visual tricks) and report which transforms were applied."
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
keywords = [
|
|
10
|
+
"deobfuscation",
|
|
11
|
+
"text",
|
|
12
|
+
"leetspeak",
|
|
13
|
+
"homoglyph",
|
|
14
|
+
"phishing",
|
|
15
|
+
"unicode",
|
|
16
|
+
"normalization",
|
|
17
|
+
"security",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"Natural Language :: English",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
31
|
+
"Topic :: Security",
|
|
32
|
+
"Typing :: Typed",
|
|
33
|
+
"Topic :: Text Processing",
|
|
34
|
+
]
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Lapid Harel"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/lapid/text-deobfuscate"
|
|
42
|
+
Repository = "https://github.com/lapid/text-deobfuscate"
|
|
43
|
+
Issues = "https://github.com/lapid/text-deobfuscate/issues"
|
|
44
|
+
Changelog = "https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md"
|
|
45
|
+
Documentation = "https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
text-deobfuscate = "text_deobfuscate.cli:main"
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.9.7,<0.14"]
|
|
52
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "text-deobfuscate"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Undo text obfuscation (encodings and visual tricks) and report which transforms were applied."
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
authors = [{ name = "Lapid Harel" }]
|
|
10
|
+
keywords = ["deobfuscation", "text", "leetspeak", "homoglyph", "phishing", "unicode", "normalization", "security"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Information Technology",
|
|
15
|
+
"Natural Language :: English",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
"Topic :: Text Processing",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/lapid/text-deobfuscate"
|
|
31
|
+
Repository = "https://github.com/lapid/text-deobfuscate"
|
|
32
|
+
Issues = "https://github.com/lapid/text-deobfuscate/issues"
|
|
33
|
+
Changelog = "https://github.com/lapid/text-deobfuscate/blob/master/CHANGELOG.md"
|
|
34
|
+
Documentation = "https://github.com/lapid/text-deobfuscate/blob/master/docs/architecture.md"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
text-deobfuscate = "text_deobfuscate.cli:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.9.7,<0.14"]
|
|
41
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Undo text obfuscation and report which transforms were applied."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata as _metadata
|
|
4
|
+
|
|
5
|
+
from text_deobfuscate.core import STRICTNESS_LEVELS, deobfuscate
|
|
6
|
+
from text_deobfuscate.types import Result, Step
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = _metadata.version("text-deobfuscate")
|
|
10
|
+
except _metadata.PackageNotFoundError: # running from a source tree that was never installed
|
|
11
|
+
__version__ = "unknown"
|
|
12
|
+
|
|
13
|
+
__all__ = ["STRICTNESS_LEVELS", "Result", "Step", "__version__", "deobfuscate"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from text_deobfuscate import __version__
|
|
6
|
+
from text_deobfuscate.core import STRICTNESS_LEVELS, deobfuscate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv: list[str] | None = None) -> int:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="text-deobfuscate",
|
|
12
|
+
description="Undo text obfuscation. Reads the text argument, or standard input if none is given.",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("text", nargs="?", help="text to deobfuscate; standard input if omitted")
|
|
15
|
+
parser.add_argument("--strictness", choices=STRICTNESS_LEVELS, default="conservative")
|
|
16
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
17
|
+
parser.add_argument("--json", action="store_true", help="print the text and the steps applied as JSON")
|
|
18
|
+
args = parser.parse_args(argv)
|
|
19
|
+
|
|
20
|
+
text = args.text if args.text is not None else sys.stdin.read()
|
|
21
|
+
result = deobfuscate(text, strictness=args.strictness)
|
|
22
|
+
if args.json:
|
|
23
|
+
json.dump(result.to_dict(), sys.stdout, ensure_ascii=False)
|
|
24
|
+
sys.stdout.write("\n")
|
|
25
|
+
else:
|
|
26
|
+
# Nothing is added when the output is piped or redirected: the contract is the
|
|
27
|
+
# text and only the text, so `text-deobfuscate x > file` writes exactly that. A
|
|
28
|
+
# terminal gets a line ending, or the shell prompt lands mid-text.
|
|
29
|
+
sys.stdout.write(result.text + "\n" if sys.stdout.isatty() else result.text)
|
|
30
|
+
return 0
|