codebread 1.0.4__tar.gz → 1.0.5__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.
- {codebread-1.0.4 → codebread-1.0.5}/PKG-INFO +51 -1
- {codebread-1.0.4 → codebread-1.0.5}/README.md +50 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/analyzer.py +8 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/connections.py +3 -1
- codebread-1.0.5/codebread/deobfuscate.py +650 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/models.py +2 -1
- {codebread-1.0.4 → codebread-1.0.5}/codebread/web/app.js +225 -45
- {codebread-1.0.4 → codebread-1.0.5}/codebread/web/index.html +6 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/web/style.css +22 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/PKG-INFO +51 -1
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/SOURCES.txt +2 -0
- {codebread-1.0.4 → codebread-1.0.5}/pyproject.toml +1 -1
- codebread-1.0.5/tests/test_deobfuscate.py +299 -0
- {codebread-1.0.4 → codebread-1.0.5}/LICENSE +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/__init__.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/classifier.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/cli.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/diff.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/export.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/languages.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/__init__.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/generic_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/javascript_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/python_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/scanner.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread/server.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/dependency_links.txt +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/entry_points.txt +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/requires.txt +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/top_level.txt +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/setup.cfg +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_analyzer.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_config_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_connections.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_generic_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_javascript_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_python_parser.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_scanner.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_server.py +0 -0
- {codebread-1.0.4 → codebread-1.0.5}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebread
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Interactive codebase analyzer & visualizer — slice open a project and see how it's wired together.
|
|
5
5
|
Author: Danial Irsyad
|
|
6
6
|
License: MIT
|
|
@@ -164,6 +164,11 @@ the map already scanned.
|
|
|
164
164
|
- **Warnings, never silence** — unreadable files, unsupported languages and
|
|
165
165
|
permission problems show up as ⚠ badges and in the warnings list, never
|
|
166
166
|
hidden. Secrets found in config files are always masked.
|
|
167
|
+
- **De-obfuscation, inline** — hex/octal escapes, `chr()`-style
|
|
168
|
+
character-code math, base64 blobs, and dead "chaff" statements get
|
|
169
|
+
statically decoded and highlighted right in the code viewer — hover a
|
|
170
|
+
🔓-underlined span to see what it originally said. See
|
|
171
|
+
[De-obfuscation](#de-obfuscation) below.
|
|
167
172
|
|
|
168
173
|
## Language support
|
|
169
174
|
|
|
@@ -200,6 +205,51 @@ Only Python's `ast`-based parser captures real parameter type annotations,
|
|
|
200
205
|
return types, and docstrings — every regex-based parser above extracts
|
|
201
206
|
parameter *names* only.
|
|
202
207
|
|
|
208
|
+
## De-obfuscation
|
|
209
|
+
|
|
210
|
+
Some codebases — commercial/nulled PHP scripts especially — ship with
|
|
211
|
+
constant-literal obfuscation: hex/octal escape sequences, `chr()`-style
|
|
212
|
+
character-code arithmetic, base64-encoded string constants, and dead
|
|
213
|
+
"chaff" statements built and thrown away purely to make the file harder to
|
|
214
|
+
read. CodeBread finds and decodes these **statically** and highlights the
|
|
215
|
+
decoded value inline in the code viewer (dashed underline — hover to see
|
|
216
|
+
the original), plus a 🔓 badge on any file with findings.
|
|
217
|
+
|
|
218
|
+
**The one rule that matters: nothing here is ever executed.** Every
|
|
219
|
+
decoder in [`codebread/deobfuscate.py`](codebread/deobfuscate.py) only
|
|
220
|
+
resolves *constant* expressions — integer literals combined with
|
|
221
|
+
`+ - * / % << >> | & ^`, inside a recognized decode call (`chr(...)`,
|
|
222
|
+
`base64_decode(...)`, ...). A restricted AST walker
|
|
223
|
+
(`safe_eval_int_expr`) is the enforcement point: if an expression touches
|
|
224
|
+
a variable, a function call, a float, or anything else that isn't a bare
|
|
225
|
+
integer literal or one of those operators, it's refused and left alone —
|
|
226
|
+
never guessed at, never evaluated by actually running the target
|
|
227
|
+
language. That's what makes it safe to point at code you don't trust yet,
|
|
228
|
+
which is the same posture as the rest of CodeBread (read-only static
|
|
229
|
+
analysis, nothing scanned is ever run).
|
|
230
|
+
|
|
231
|
+
| Language | Hex `\xHH` | Octal `\NNN` | Unicode `\uHHHH` / `\u{HHHH}` | Char-code math | Base64 literals | Dead "chaff" statements |
|
|
232
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
233
|
+
| Python | ✅ | ✅ | ✅ `\uHHHH` | ✅ `chr(...)` | ✅ `base64.b64decode(...)` | –³ |
|
|
234
|
+
| JavaScript / TypeScript / Vue / Svelte | ✅ | ✅ | ✅ both forms | ✅ `String.fromCharCode` / `fromCodePoint` | ✅ `atob(...)` | ✅ (`+` concat) |
|
|
235
|
+
| Java | – | ✅ | ✅ `\uHHHH` | ✅ `(char)(...)` cast | ✅ `Base64.getDecoder().decode(...)` | ✅ (`+` concat) |
|
|
236
|
+
| C# | ✅ | – | ✅ `\uHHHH` | ✅ `(char)(...)` cast | ✅ `Convert.FromBase64String(...)` | ✅ (`+` concat) |
|
|
237
|
+
| Go | ✅ | ✅ | ✅ `\uHHHH` | ✅ `string(rune(...))` | ✅ `base64.StdEncoding.DecodeString(...)` | ✅ (`+` concat) |
|
|
238
|
+
| PHP | ✅ | ✅ | ✅ `\u{HHHH}` only | ✅ `chr(...)` | ✅ `base64_decode(...)` | ✅ (`.` concat) |
|
|
239
|
+
| Ruby | ✅ | ✅ | ✅ both forms | ✅ `<int>.chr` | ✅ `Base64.decode64(...)` | –³ |
|
|
240
|
+
|
|
241
|
+
Decoded base64 output is re-checked against the same credential-masking
|
|
242
|
+
rule used on config files (`URL_CREDS_RE`) — revealing a hidden literal
|
|
243
|
+
can't be used to route around the "credentials always masked" guarantee
|
|
244
|
+
elsewhere in the tool.
|
|
245
|
+
|
|
246
|
+
³ Chaff detection needs a reliable statement boundary to prove "this
|
|
247
|
+
literal chain is the *entire* statement, nothing else on either side" —
|
|
248
|
+
the `;`-terminated languages above have one. Python and Ruby don't require
|
|
249
|
+
a terminator, so v1 intentionally reports nothing there rather than guess
|
|
250
|
+
and risk false positives; the escape/char-code/base64 decoders still work
|
|
251
|
+
fully on both.
|
|
252
|
+
|
|
203
253
|
## How it classifies layers
|
|
204
254
|
|
|
205
255
|
Score-based heuristics combining framework import signatures
|
|
@@ -148,6 +148,11 @@ the map already scanned.
|
|
|
148
148
|
- **Warnings, never silence** — unreadable files, unsupported languages and
|
|
149
149
|
permission problems show up as ⚠ badges and in the warnings list, never
|
|
150
150
|
hidden. Secrets found in config files are always masked.
|
|
151
|
+
- **De-obfuscation, inline** — hex/octal escapes, `chr()`-style
|
|
152
|
+
character-code math, base64 blobs, and dead "chaff" statements get
|
|
153
|
+
statically decoded and highlighted right in the code viewer — hover a
|
|
154
|
+
🔓-underlined span to see what it originally said. See
|
|
155
|
+
[De-obfuscation](#de-obfuscation) below.
|
|
151
156
|
|
|
152
157
|
## Language support
|
|
153
158
|
|
|
@@ -184,6 +189,51 @@ Only Python's `ast`-based parser captures real parameter type annotations,
|
|
|
184
189
|
return types, and docstrings — every regex-based parser above extracts
|
|
185
190
|
parameter *names* only.
|
|
186
191
|
|
|
192
|
+
## De-obfuscation
|
|
193
|
+
|
|
194
|
+
Some codebases — commercial/nulled PHP scripts especially — ship with
|
|
195
|
+
constant-literal obfuscation: hex/octal escape sequences, `chr()`-style
|
|
196
|
+
character-code arithmetic, base64-encoded string constants, and dead
|
|
197
|
+
"chaff" statements built and thrown away purely to make the file harder to
|
|
198
|
+
read. CodeBread finds and decodes these **statically** and highlights the
|
|
199
|
+
decoded value inline in the code viewer (dashed underline — hover to see
|
|
200
|
+
the original), plus a 🔓 badge on any file with findings.
|
|
201
|
+
|
|
202
|
+
**The one rule that matters: nothing here is ever executed.** Every
|
|
203
|
+
decoder in [`codebread/deobfuscate.py`](codebread/deobfuscate.py) only
|
|
204
|
+
resolves *constant* expressions — integer literals combined with
|
|
205
|
+
`+ - * / % << >> | & ^`, inside a recognized decode call (`chr(...)`,
|
|
206
|
+
`base64_decode(...)`, ...). A restricted AST walker
|
|
207
|
+
(`safe_eval_int_expr`) is the enforcement point: if an expression touches
|
|
208
|
+
a variable, a function call, a float, or anything else that isn't a bare
|
|
209
|
+
integer literal or one of those operators, it's refused and left alone —
|
|
210
|
+
never guessed at, never evaluated by actually running the target
|
|
211
|
+
language. That's what makes it safe to point at code you don't trust yet,
|
|
212
|
+
which is the same posture as the rest of CodeBread (read-only static
|
|
213
|
+
analysis, nothing scanned is ever run).
|
|
214
|
+
|
|
215
|
+
| Language | Hex `\xHH` | Octal `\NNN` | Unicode `\uHHHH` / `\u{HHHH}` | Char-code math | Base64 literals | Dead "chaff" statements |
|
|
216
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
217
|
+
| Python | ✅ | ✅ | ✅ `\uHHHH` | ✅ `chr(...)` | ✅ `base64.b64decode(...)` | –³ |
|
|
218
|
+
| JavaScript / TypeScript / Vue / Svelte | ✅ | ✅ | ✅ both forms | ✅ `String.fromCharCode` / `fromCodePoint` | ✅ `atob(...)` | ✅ (`+` concat) |
|
|
219
|
+
| Java | – | ✅ | ✅ `\uHHHH` | ✅ `(char)(...)` cast | ✅ `Base64.getDecoder().decode(...)` | ✅ (`+` concat) |
|
|
220
|
+
| C# | ✅ | – | ✅ `\uHHHH` | ✅ `(char)(...)` cast | ✅ `Convert.FromBase64String(...)` | ✅ (`+` concat) |
|
|
221
|
+
| Go | ✅ | ✅ | ✅ `\uHHHH` | ✅ `string(rune(...))` | ✅ `base64.StdEncoding.DecodeString(...)` | ✅ (`+` concat) |
|
|
222
|
+
| PHP | ✅ | ✅ | ✅ `\u{HHHH}` only | ✅ `chr(...)` | ✅ `base64_decode(...)` | ✅ (`.` concat) |
|
|
223
|
+
| Ruby | ✅ | ✅ | ✅ both forms | ✅ `<int>.chr` | ✅ `Base64.decode64(...)` | –³ |
|
|
224
|
+
|
|
225
|
+
Decoded base64 output is re-checked against the same credential-masking
|
|
226
|
+
rule used on config files (`URL_CREDS_RE`) — revealing a hidden literal
|
|
227
|
+
can't be used to route around the "credentials always masked" guarantee
|
|
228
|
+
elsewhere in the tool.
|
|
229
|
+
|
|
230
|
+
³ Chaff detection needs a reliable statement boundary to prove "this
|
|
231
|
+
literal chain is the *entire* statement, nothing else on either side" —
|
|
232
|
+
the `;`-terminated languages above have one. Python and Ruby don't require
|
|
233
|
+
a terminator, so v1 intentionally reports nothing there rather than guess
|
|
234
|
+
and risk false positives; the escape/char-code/base64 decoders still work
|
|
235
|
+
fully on both.
|
|
236
|
+
|
|
187
237
|
## How it classifies layers
|
|
188
238
|
|
|
189
239
|
Score-based heuristics combining framework import signatures
|
|
@@ -9,6 +9,7 @@ from typing import Callable, Dict, Optional
|
|
|
9
9
|
from . import __version__
|
|
10
10
|
from .classifier import classify
|
|
11
11
|
from .connections import build_graph
|
|
12
|
+
from .deobfuscate import scan_obfuscation
|
|
12
13
|
from .parsers import parse_file
|
|
13
14
|
from .scanner import read_text, scan
|
|
14
15
|
|
|
@@ -40,6 +41,11 @@ def analyze(root: str,
|
|
|
40
41
|
from .parsers.generic_parser import redact_secrets
|
|
41
42
|
src = redact_secrets(src)
|
|
42
43
|
info.source = src
|
|
44
|
+
if text:
|
|
45
|
+
info.obfuscation = [
|
|
46
|
+
{"line": f.line, "kind": f.kind, "original": f.original, "decoded": f.decoded}
|
|
47
|
+
for f in scan_obfuscation(text, meta["language"])
|
|
48
|
+
]
|
|
43
49
|
parsed.append(info)
|
|
44
50
|
for w in info.warnings:
|
|
45
51
|
if w.startswith("Unsupported:"):
|
|
@@ -78,6 +84,8 @@ def _annotate_tree(node: Dict, by_path: Dict) -> None:
|
|
|
78
84
|
node["nFunctions"] = len(info.functions)
|
|
79
85
|
if info.warnings and "warning" not in node:
|
|
80
86
|
node["warning"] = "parse"
|
|
87
|
+
if info.obfuscation:
|
|
88
|
+
node["obfuscated"] = True
|
|
81
89
|
return
|
|
82
90
|
layers = set()
|
|
83
91
|
for child in node.get("children", []):
|
|
@@ -209,7 +209,7 @@ def build_graph(files: List[FileInfo], tree: Dict,
|
|
|
209
209
|
"loc": f.loc, "warnings": f.warnings,
|
|
210
210
|
"nFunctions": len(f.functions),
|
|
211
211
|
"imports": f.imports[:30], "dbConfig": f.db_config,
|
|
212
|
-
"source": f.source,
|
|
212
|
+
"source": f.source, "obfuscation": f.obfuscation,
|
|
213
213
|
})
|
|
214
214
|
for fn in f.functions:
|
|
215
215
|
nid = f"{f.path}::{fn.name}@{fn.line}"
|
|
@@ -393,6 +393,7 @@ def build_graph(files: List[FileInfo], tree: Dict,
|
|
|
393
393
|
# ---- stats ---------------------------------------------------------
|
|
394
394
|
n_fns = sum(1 for n in nodes if n["kind"] in ("function", "method"))
|
|
395
395
|
n_orphans = sum(1 for n in nodes if n.get("orphan"))
|
|
396
|
+
n_obfuscated = sum(1 for f in files if f.obfuscation)
|
|
396
397
|
stats = {
|
|
397
398
|
"files": len(files),
|
|
398
399
|
"functions": n_fns,
|
|
@@ -402,6 +403,7 @@ def build_graph(files: List[FileInfo], tree: Dict,
|
|
|
402
403
|
"warnings": len(warnings) + sum(len(f.warnings) for f in files),
|
|
403
404
|
"orphans": n_orphans,
|
|
404
405
|
"cycles": len(cycles),
|
|
406
|
+
"obfuscated": n_obfuscated,
|
|
405
407
|
}
|
|
406
408
|
return {"nodes": nodes, "edges": edges, "tree": tree,
|
|
407
409
|
"warnings": warnings, "cycles": cycles, "stats": stats}
|