snakecharm 0.1.1__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 (47) hide show
  1. snakecharm-0.1.1/.gitignore +21 -0
  2. snakecharm-0.1.1/LICENSE +21 -0
  3. snakecharm-0.1.1/PKG-INFO +203 -0
  4. snakecharm-0.1.1/README.md +183 -0
  5. snakecharm-0.1.1/pyproject.toml +117 -0
  6. snakecharm-0.1.1/snakecharm/__init__.py +385 -0
  7. snakecharm-0.1.1/snakecharm/clean/__init__.py +26 -0
  8. snakecharm-0.1.1/snakecharm/clean/passes.py +352 -0
  9. snakecharm-0.1.1/snakecharm/clean/rename_survey.py +174 -0
  10. snakecharm-0.1.1/snakecharm/clean/session.py +239 -0
  11. snakecharm-0.1.1/snakecharm/cli.py +327 -0
  12. snakecharm-0.1.1/snakecharm/decompile/__init__.py +20 -0
  13. snakecharm-0.1.1/snakecharm/decompile/backends.py +146 -0
  14. snakecharm-0.1.1/snakecharm/decompile/router.py +154 -0
  15. snakecharm-0.1.1/snakecharm/errors.py +84 -0
  16. snakecharm-0.1.1/snakecharm/evalx/__init__.py +0 -0
  17. snakecharm-0.1.1/snakecharm/evalx/budgets.py +265 -0
  18. snakecharm-0.1.1/snakecharm/evalx/interp.py +1280 -0
  19. snakecharm-0.1.1/snakecharm/evalx/resolve.py +263 -0
  20. snakecharm-0.1.1/snakecharm/evalx/whitelist.py +748 -0
  21. snakecharm-0.1.1/snakecharm/ingest/__init__.py +0 -0
  22. snakecharm-0.1.1/snakecharm/ingest/normalize.py +184 -0
  23. snakecharm-0.1.1/snakecharm/ingest/pyc.py +90 -0
  24. snakecharm-0.1.1/snakecharm/ingest/registry.py +117 -0
  25. snakecharm-0.1.1/snakecharm/ingest/source.py +73 -0
  26. snakecharm-0.1.1/snakecharm/ir/__init__.py +0 -0
  27. snakecharm-0.1.1/snakecharm/ir/codeunit.py +186 -0
  28. snakecharm-0.1.1/snakecharm/ir/disasm.py +295 -0
  29. snakecharm-0.1.1/snakecharm/ir/gen_tables.py +287 -0
  30. snakecharm-0.1.1/snakecharm/ir/marshal_.py +450 -0
  31. snakecharm-0.1.1/snakecharm/ir/tables/py310.json +765 -0
  32. snakecharm-0.1.1/snakecharm/ir/tables/py311.json +944 -0
  33. snakecharm-0.1.1/snakecharm/ir/tables/py312.json +1122 -0
  34. snakecharm-0.1.1/snakecharm/ir/tables/py313.json +1145 -0
  35. snakecharm-0.1.1/snakecharm/ir/tables/py314.json +1175 -0
  36. snakecharm-0.1.1/snakecharm/ir/tables/py38.json +460 -0
  37. snakecharm-0.1.1/snakecharm/ir/tables/py39.json +453 -0
  38. snakecharm-0.1.1/snakecharm/peel/__init__.py +0 -0
  39. snakecharm-0.1.1/snakecharm/peel/driver.py +341 -0
  40. snakecharm-0.1.1/snakecharm/peel/layer.py +73 -0
  41. snakecharm-0.1.1/snakecharm/report/__init__.py +0 -0
  42. snakecharm-0.1.1/snakecharm/report/emit.py +83 -0
  43. snakecharm-0.1.1/snakecharm/triage/__init__.py +0 -0
  44. snakecharm-0.1.1/snakecharm/triage/db/families.yaml +288 -0
  45. snakecharm-0.1.1/snakecharm/triage/detect.py +321 -0
  46. snakecharm-0.1.1/snakecharm/triage/signatures.py +276 -0
  47. snakecharm-0.1.1/snakecharm/unresolved.py +116 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ .coverage.*
8
+ htmlcov/
9
+ *.egg-info/
10
+
11
+ # The fixture corpus is a build artifact of tests/make_corpus.py, regenerated
12
+ # on demand by conftest. Half-committing it is how a fresh clone ended up with
13
+ # a corpus that looked complete and was missing real.pyc.
14
+ tests/corpus/
15
+ dist/
16
+ build/
17
+
18
+ # Research area: obfuscator tools, generated fixtures, and live malware samples.
19
+ # Nothing in here is ever committed. Samples are third-party code we have no
20
+ # right to redistribute, and `git add -A` would put them in history permanently.
21
+ research/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 snakecharm
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,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: snakecharm
3
+ Version: 0.1.1
4
+ Summary: Deobfuscate Python without executing it. A webcrack for Python.
5
+ Project-URL: Homepage, https://veryserious.systems
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: deobfuscation,malware-analysis,obfuscation,python,reverse-engineering
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Information Technology
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: pyyaml>=6.0
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: typer>=0.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # snakecharm
22
+
23
+ **Python De-Obfuscation**
24
+
25
+ Unwraps obfuscated Python — `exec`/`eval`/`compile` chains over base64, hex,
26
+ zlib/gzip/bz2/lzma, XOR, `marshal`, reversal and `chr`/`ord` reconstruction —
27
+ without executing the sample.
28
+
29
+ Requires Python 3.11+. Pure Python; depends on `typer`, `rich`, `pyyaml`.
30
+
31
+ ## Install
32
+
33
+ ```console
34
+ pip install snakecharm
35
+ uv tool install snakecharm # isolated, puts snakecharm on PATH
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ```console
41
+ snakecharm crack FILE [-o DIR] [--json] [--clean]
42
+ snakecharm detect FILE [--json]
43
+ snakecharm inspect FILE [--target-python X.Y]
44
+ snakecharm version
45
+ ```
46
+
47
+ | | |
48
+ |---|---|
49
+ | `crack` | Unwrap. Recovered source to stdout, diagnostics to stderr. |
50
+ | `detect` | Fingerprint the obfuscator family; do not unwrap. |
51
+ | `inspect` | Disassemble a `.pyc` or bare marshal stream. |
52
+ | `version` | Print the version. |
53
+
54
+ `FILE` may be `-` to read from stdin.
55
+
56
+ Cleanup is a flag on `crack` (`--clean`), not a separate command.
57
+
58
+ ### `crack` options
59
+
60
+ | Flag | Default | Meaning |
61
+ |---|---|---|
62
+ | `-o, --output DIR` | — | Write the full artifact tree instead of stdout. |
63
+ | `--json` | off | Emit `report.json` to stdout. |
64
+ | `--clean` | off | Also emit a cleaned view (see [Cleanup](#cleanup)). |
65
+ | `--target-python X.Y` | host | Version to assume for marshal payloads. |
66
+ | `--decompiler NAME` | auto | Force a backend instead of routing by version. |
67
+ | `--max-layers N` | 512 | Layer cap; `0` for no limit. |
68
+ | `--max-memory MiB` | 1024 | Total decoded output; `0` for no limit. |
69
+ | `--max-fold MiB` | 256 | Largest single decoded value; `0` for no limit. |
70
+ | `--timeout SEC` | 300 | Wall clock; `0` for no limit. |
71
+ | `--no-peel` | off | Skip unwrapping; report detection only. |
72
+ | `--no-detect` | off | Skip fingerprinting. |
73
+ | `--plugins` | off | Load third-party ingest adapters. |
74
+ | `-q, --quiet` | off | Suppress the stderr summary. |
75
+
76
+ Exit codes: `0` complete, `1` incomplete, `2` input could not be processed.
77
+ Identifying the obfuscator does not affect the exit code.
78
+
79
+ ## Output
80
+
81
+ ```console
82
+ $ snakecharm crack sample.py
83
+ import os
84
+ import sys
85
+
86
+ MESSAGE = "recovered exactly"
87
+ LIMIT = 42
88
+ ...
89
+
90
+ 31 layers, depth 30, 431,204 bytes folded in 0.006s
91
+ detected: freecodingtools (100%)
92
+ ```
93
+
94
+ With `-o`:
95
+
96
+ ```console
97
+ $ snakecharm crack sample.py -o out/
98
+ wrote out/
99
+ 2 layers, depth 1, 1,966 bytes folded in 0.001s
100
+ detected: hyperion (100%), freecodingtools (67%)
101
+
102
+ $ find out -type f
103
+ out/deobfuscated.py
104
+ out/layers/00_depth0.py
105
+ out/layers/01_depth1.py
106
+ out/report.json
107
+ ```
108
+
109
+ `--clean` adds `out/cleaned.py`. `layers/` stays byte-exact.
110
+
111
+ Each layer file carries its derivation:
112
+
113
+ ```python
114
+ # ---------------------------------------------------------------
115
+ # snakecharm layer 1 (depth 1, source)
116
+ # provenance : zlib <- b64decode
117
+ # from : exec() at line 13, col 0 of layer 0
118
+ # sha256 : 58dda7518dfd60b35d3b522a654ad6c8532b076846f3a37d98340b05f2131acd
119
+ # ---------------------------------------------------------------
120
+ ```
121
+
122
+ When a site cannot be resolved it is reported, not skipped:
123
+
124
+ ```console
125
+ $ snakecharm crack script.py
126
+ not_reached : could not fold the argument of exec(): not reached during
127
+ interpretation, and folding it in isolation also failed: unbound name 'handle'
128
+ incomplete: 1 site(s) could not be resolved statically
129
+ ```
130
+
131
+ ## Library
132
+
133
+ ```python
134
+ import snakecharm
135
+
136
+ result = snakecharm.crack(open("sample.py", "rb").read())
137
+
138
+ result.code # deepest recovered source
139
+ result.complete # bool
140
+ result.unresolved # [Unresolved(reason, message, layer, line, col)]
141
+ result.layers # [Layer(...)], each with .provenance and .digest
142
+ result.detections # [Detection(family, confidence, evidence)]
143
+ result.stats # {'layers': 31, 'max_depth': 30, 'bytes_folded': 431204, ...}
144
+ result.cleaned # str | None, when Options(clean=True)
145
+ result.save("out/") # writes the tree above
146
+ ```
147
+
148
+ `snakecharm.Options` mirrors the CLI flags. `snakecharm.Budget` holds the
149
+ resource limits.
150
+
151
+ To run the cleanup passes on their own, without peeling:
152
+
153
+ ```python
154
+ from snakecharm.clean import clean
155
+
156
+ result = clean("X = 1 + 2\n")
157
+ result.code # 'X = 3'
158
+ result.changed, result.refused, result.rolled_back
159
+ ```
160
+
161
+ ## Handles
162
+
163
+ - `exec`/`eval`/`compile` chains nested to arbitrary depth (default cap 512).
164
+ - Wrappers: `try`/`except`, `with`, class bodies, `if __name__ == "__main__"`,
165
+ `async def`, decorators.
166
+ - `.pyc` files and bare marshal streams, Python 3.8–3.14, from any host version.
167
+ - PEP 263 source encodings, decoded before analysis and recorded in the
168
+ provenance chain.
169
+ - Family fingerprinting: freecodingtools/pyobfuscate.com, Hyperion, Berserker,
170
+ Kramer, BlankOBF, Jawbreaker, PlusOBF, Vare, Anubis, Mr.X, PyArmor, Intensio,
171
+ Nuitka, PyInstaller, python-minifier. Detection is additive — an unrecognised
172
+ sample is still unwrapped. Signatures not confirmed against real output are
173
+ marked `[unvalidated signature]`.
174
+
175
+ Nothing in the default path executes sample code. Analysing live malware in a
176
+ disposable VM is still advisable.
177
+
178
+ ### Cleanup
179
+
180
+ `--clean` runs, in order: constant folding, `getattr(o, "lit")` → `o.lit`,
181
+ removal of branches whose condition folds to a constant, and removal of unused
182
+ function locals.
183
+
184
+ Module-level globals and uncalled functions are never removed. A round that
185
+ fails to parse or compile is rolled back whole.
186
+
187
+ ## Does not handle
188
+
189
+ | | |
190
+ |---|---|
191
+ | Accurate 3.11+ decompilation | No released decompiler handles it. Falls back to annotated disassembly, labelled as such. |
192
+ | Identifier renaming | Mangled names are returned unchanged. |
193
+ | Packed executables | PyInstaller / cx_Freeze / py2exe are not unpacked. `detect` still names the packer. |
194
+ | Nuitka | Native machine code; no `.pyc` exists in the binary. Named, not recovered. |
195
+ | PyArmor BCC / RFT | Native C, and one-way identifier destruction, respectively. |
196
+ | MBA obfuscation | Arithmetic seeded with live in-scope variables defeats static folding. |
197
+ | Runtime-fetched payloads | Not present in the file. |
198
+
199
+ Decompiler backends (`uncompyle6`, `decompyle3`, `pycdc`) are used if found on
200
+ `PATH`. They are GPL, so they are never bundled, never imported, and invoked
201
+ only as subprocesses; without them, `.pyc` input renders as disassembly.
202
+
203
+ MIT. Created and provided by veryserious.systems.
@@ -0,0 +1,183 @@
1
+ # snakecharm
2
+
3
+ **Python De-Obfuscation**
4
+
5
+ Unwraps obfuscated Python — `exec`/`eval`/`compile` chains over base64, hex,
6
+ zlib/gzip/bz2/lzma, XOR, `marshal`, reversal and `chr`/`ord` reconstruction —
7
+ without executing the sample.
8
+
9
+ Requires Python 3.11+. Pure Python; depends on `typer`, `rich`, `pyyaml`.
10
+
11
+ ## Install
12
+
13
+ ```console
14
+ pip install snakecharm
15
+ uv tool install snakecharm # isolated, puts snakecharm on PATH
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ ```console
21
+ snakecharm crack FILE [-o DIR] [--json] [--clean]
22
+ snakecharm detect FILE [--json]
23
+ snakecharm inspect FILE [--target-python X.Y]
24
+ snakecharm version
25
+ ```
26
+
27
+ | | |
28
+ |---|---|
29
+ | `crack` | Unwrap. Recovered source to stdout, diagnostics to stderr. |
30
+ | `detect` | Fingerprint the obfuscator family; do not unwrap. |
31
+ | `inspect` | Disassemble a `.pyc` or bare marshal stream. |
32
+ | `version` | Print the version. |
33
+
34
+ `FILE` may be `-` to read from stdin.
35
+
36
+ Cleanup is a flag on `crack` (`--clean`), not a separate command.
37
+
38
+ ### `crack` options
39
+
40
+ | Flag | Default | Meaning |
41
+ |---|---|---|
42
+ | `-o, --output DIR` | — | Write the full artifact tree instead of stdout. |
43
+ | `--json` | off | Emit `report.json` to stdout. |
44
+ | `--clean` | off | Also emit a cleaned view (see [Cleanup](#cleanup)). |
45
+ | `--target-python X.Y` | host | Version to assume for marshal payloads. |
46
+ | `--decompiler NAME` | auto | Force a backend instead of routing by version. |
47
+ | `--max-layers N` | 512 | Layer cap; `0` for no limit. |
48
+ | `--max-memory MiB` | 1024 | Total decoded output; `0` for no limit. |
49
+ | `--max-fold MiB` | 256 | Largest single decoded value; `0` for no limit. |
50
+ | `--timeout SEC` | 300 | Wall clock; `0` for no limit. |
51
+ | `--no-peel` | off | Skip unwrapping; report detection only. |
52
+ | `--no-detect` | off | Skip fingerprinting. |
53
+ | `--plugins` | off | Load third-party ingest adapters. |
54
+ | `-q, --quiet` | off | Suppress the stderr summary. |
55
+
56
+ Exit codes: `0` complete, `1` incomplete, `2` input could not be processed.
57
+ Identifying the obfuscator does not affect the exit code.
58
+
59
+ ## Output
60
+
61
+ ```console
62
+ $ snakecharm crack sample.py
63
+ import os
64
+ import sys
65
+
66
+ MESSAGE = "recovered exactly"
67
+ LIMIT = 42
68
+ ...
69
+
70
+ 31 layers, depth 30, 431,204 bytes folded in 0.006s
71
+ detected: freecodingtools (100%)
72
+ ```
73
+
74
+ With `-o`:
75
+
76
+ ```console
77
+ $ snakecharm crack sample.py -o out/
78
+ wrote out/
79
+ 2 layers, depth 1, 1,966 bytes folded in 0.001s
80
+ detected: hyperion (100%), freecodingtools (67%)
81
+
82
+ $ find out -type f
83
+ out/deobfuscated.py
84
+ out/layers/00_depth0.py
85
+ out/layers/01_depth1.py
86
+ out/report.json
87
+ ```
88
+
89
+ `--clean` adds `out/cleaned.py`. `layers/` stays byte-exact.
90
+
91
+ Each layer file carries its derivation:
92
+
93
+ ```python
94
+ # ---------------------------------------------------------------
95
+ # snakecharm layer 1 (depth 1, source)
96
+ # provenance : zlib <- b64decode
97
+ # from : exec() at line 13, col 0 of layer 0
98
+ # sha256 : 58dda7518dfd60b35d3b522a654ad6c8532b076846f3a37d98340b05f2131acd
99
+ # ---------------------------------------------------------------
100
+ ```
101
+
102
+ When a site cannot be resolved it is reported, not skipped:
103
+
104
+ ```console
105
+ $ snakecharm crack script.py
106
+ not_reached : could not fold the argument of exec(): not reached during
107
+ interpretation, and folding it in isolation also failed: unbound name 'handle'
108
+ incomplete: 1 site(s) could not be resolved statically
109
+ ```
110
+
111
+ ## Library
112
+
113
+ ```python
114
+ import snakecharm
115
+
116
+ result = snakecharm.crack(open("sample.py", "rb").read())
117
+
118
+ result.code # deepest recovered source
119
+ result.complete # bool
120
+ result.unresolved # [Unresolved(reason, message, layer, line, col)]
121
+ result.layers # [Layer(...)], each with .provenance and .digest
122
+ result.detections # [Detection(family, confidence, evidence)]
123
+ result.stats # {'layers': 31, 'max_depth': 30, 'bytes_folded': 431204, ...}
124
+ result.cleaned # str | None, when Options(clean=True)
125
+ result.save("out/") # writes the tree above
126
+ ```
127
+
128
+ `snakecharm.Options` mirrors the CLI flags. `snakecharm.Budget` holds the
129
+ resource limits.
130
+
131
+ To run the cleanup passes on their own, without peeling:
132
+
133
+ ```python
134
+ from snakecharm.clean import clean
135
+
136
+ result = clean("X = 1 + 2\n")
137
+ result.code # 'X = 3'
138
+ result.changed, result.refused, result.rolled_back
139
+ ```
140
+
141
+ ## Handles
142
+
143
+ - `exec`/`eval`/`compile` chains nested to arbitrary depth (default cap 512).
144
+ - Wrappers: `try`/`except`, `with`, class bodies, `if __name__ == "__main__"`,
145
+ `async def`, decorators.
146
+ - `.pyc` files and bare marshal streams, Python 3.8–3.14, from any host version.
147
+ - PEP 263 source encodings, decoded before analysis and recorded in the
148
+ provenance chain.
149
+ - Family fingerprinting: freecodingtools/pyobfuscate.com, Hyperion, Berserker,
150
+ Kramer, BlankOBF, Jawbreaker, PlusOBF, Vare, Anubis, Mr.X, PyArmor, Intensio,
151
+ Nuitka, PyInstaller, python-minifier. Detection is additive — an unrecognised
152
+ sample is still unwrapped. Signatures not confirmed against real output are
153
+ marked `[unvalidated signature]`.
154
+
155
+ Nothing in the default path executes sample code. Analysing live malware in a
156
+ disposable VM is still advisable.
157
+
158
+ ### Cleanup
159
+
160
+ `--clean` runs, in order: constant folding, `getattr(o, "lit")` → `o.lit`,
161
+ removal of branches whose condition folds to a constant, and removal of unused
162
+ function locals.
163
+
164
+ Module-level globals and uncalled functions are never removed. A round that
165
+ fails to parse or compile is rolled back whole.
166
+
167
+ ## Does not handle
168
+
169
+ | | |
170
+ |---|---|
171
+ | Accurate 3.11+ decompilation | No released decompiler handles it. Falls back to annotated disassembly, labelled as such. |
172
+ | Identifier renaming | Mangled names are returned unchanged. |
173
+ | Packed executables | PyInstaller / cx_Freeze / py2exe are not unpacked. `detect` still names the packer. |
174
+ | Nuitka | Native machine code; no `.pyc` exists in the binary. Named, not recovered. |
175
+ | PyArmor BCC / RFT | Native C, and one-way identifier destruction, respectively. |
176
+ | MBA obfuscation | Arithmetic seeded with live in-scope variables defeats static folding. |
177
+ | Runtime-fetched payloads | Not present in the file. |
178
+
179
+ Decompiler backends (`uncompyle6`, `decompyle3`, `pycdc`) are used if found on
180
+ `PATH`. They are GPL, so they are never bundled, never imported, and invoked
181
+ only as subprocesses; without them, `.pyc` input renders as disassembly.
182
+
183
+ MIT. Created and provided by veryserious.systems.
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "snakecharm"
7
+ dynamic = ["version"]
8
+ description = "Deobfuscate Python without executing it. A webcrack for Python."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ keywords = ["deobfuscation", "malware-analysis", "reverse-engineering", "python", "obfuscation"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Information Technology",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Security",
20
+ ]
21
+
22
+ # Core stays deliberately small. libcst is not needed until the v0.2 CST tier.
23
+ dependencies = [
24
+ "typer>=0.12",
25
+ "rich>=13.0",
26
+ "pyyaml>=6.0",
27
+ ]
28
+
29
+ # No optional extras. `ast-grep-py` and `libcst` were both declared here and
30
+ # never imported -- a dependency you do not use is a claim you cannot back.
31
+ # Structural detection uses stdlib `ast`; the v0.2 cleanup tier uses `ast` +
32
+ # `ast.unparse` by design.
33
+
34
+ # Must stay below every `[project]` key: a TOML sub-table claims everything
35
+ # after it, so putting this higher silently swallowed `dependencies`.
36
+ [project.urls]
37
+ Homepage = "https://veryserious.systems"
38
+
39
+ [dependency-groups]
40
+ dev = ["pytest>=8.0", "hypothesis>=6.100"]
41
+ # Real obfuscators, used to generate the Tier-2 ground-truth corpus. Both MIT.
42
+ # Not needed to run snakecharm -- only to regenerate tests/corpus_real/.
43
+ obfuscators = ["python-minifier>=2.11", "intensio-obfuscator>=1.0.10"]
44
+
45
+ [project.scripts]
46
+ snakecharm = "snakecharm.cli:main"
47
+ snakecharm-gen-tables = "snakecharm.ir.gen_tables:main"
48
+
49
+ [tool.hatch.version]
50
+ path = "snakecharm/__init__.py"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["snakecharm"]
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ # Allowlist, not denylist. The previous exclude-list shipped a 53 KB .coverage
57
+ # database full of this machine's absolute paths, because it was untracked and
58
+ # therefore not something anyone had thought to exclude.
59
+ #
60
+ # `/tests` is deliberately absent. PyPI rejects the sdist with "PyArmor-
61
+ # encrypted content is not allowed" because tests/test_packers.py holds a real
62
+ # two-line PyArmor stub -- the exact shape a protected file has, which is the
63
+ # whole reason it is a useful fixture for testing that we detect PyArmor. The
64
+ # fixture is not reshaped to get past the scanner: that would be evading a
65
+ # security control, and a fixture that no longer looks like the thing it stands
66
+ # for tests nothing. Tests are not needed to install or use the package, so they
67
+ # stay in the repository and out of the distribution.
68
+ include = [
69
+ "/snakecharm",
70
+ "/README.md",
71
+ "/LICENSE",
72
+ "/pyproject.toml",
73
+ ]
74
+ exclude = ["*.pyc", "__pycache__"]
75
+
76
+ [tool.ruff]
77
+ line-length = 100
78
+ # The corpus is deliberately malformed, obfuscated, or not text at all.
79
+ # Linting it is meaningless and its failures drown out real ones.
80
+ # Both corpora are deliberately obfuscated or malformed. Linting them is
81
+ # meaningless and their errors drown out real ones.
82
+ extend-exclude = ["tests/corpus", "tests/corpus_real"]
83
+
84
+ [tool.ruff.lint]
85
+ select = ["F", "E", "W", "I", "UP", "B", "SIM"]
86
+ ignore = [
87
+ "E501", # long lines: the signature tables and opcode maps read better wide
88
+ "B008", # typer's Option()-in-default is the documented idiom
89
+ "SIM105", # contextlib.suppress is not clearer in tight interpreter loops
90
+ "SIM905", # a wrapped "a b c".split() reads better than a 30-element list literal
91
+ ]
92
+
93
+ [tool.pytest.ini_options]
94
+ testpaths = ["tests"]
95
+ pythonpath = ["tests"]
96
+ markers = [
97
+ "slow: bomb/fuzz cases that allocate real memory",
98
+ ]
99
+ filterwarnings = ["error::DeprecationWarning"]
100
+
101
+ [tool.pyright]
102
+ typeCheckingMode = "strict"
103
+ pythonVersion = "3.11"
104
+ include = ["snakecharm", "tests"]
105
+ # Third-party samples and obfuscator tools, never ours to type-check.
106
+ exclude = ["research", "**/__pycache__", "tests/corpus", "tests/corpus_real"]
107
+ # Without this the CLI resolves an ephemeral interpreter instead of the project
108
+ # venv, so typer/rich/pytest go unresolved and every value derived from them
109
+ # degrades to Unknown -- 1,219 diagnostics, most of them that one cascade.
110
+ venvPath = "."
111
+ venv = ".venv"
112
+ # Every module-private function in this codebase that looks unused is reached
113
+ # through a registration decorator -- @primitive in the whitelist, @register in
114
+ # the cleanup passes -- and is never named again after its definition. All 28
115
+ # reports were that pattern, so the rule cannot tell dead code from the central
116
+ # idiom here.
117
+ reportUnusedFunction = false