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.
Files changed (40) hide show
  1. {codebread-1.0.4 → codebread-1.0.5}/PKG-INFO +51 -1
  2. {codebread-1.0.4 → codebread-1.0.5}/README.md +50 -0
  3. {codebread-1.0.4 → codebread-1.0.5}/codebread/analyzer.py +8 -0
  4. {codebread-1.0.4 → codebread-1.0.5}/codebread/connections.py +3 -1
  5. codebread-1.0.5/codebread/deobfuscate.py +650 -0
  6. {codebread-1.0.4 → codebread-1.0.5}/codebread/models.py +2 -1
  7. {codebread-1.0.4 → codebread-1.0.5}/codebread/web/app.js +225 -45
  8. {codebread-1.0.4 → codebread-1.0.5}/codebread/web/index.html +6 -0
  9. {codebread-1.0.4 → codebread-1.0.5}/codebread/web/style.css +22 -0
  10. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/PKG-INFO +51 -1
  11. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/SOURCES.txt +2 -0
  12. {codebread-1.0.4 → codebread-1.0.5}/pyproject.toml +1 -1
  13. codebread-1.0.5/tests/test_deobfuscate.py +299 -0
  14. {codebread-1.0.4 → codebread-1.0.5}/LICENSE +0 -0
  15. {codebread-1.0.4 → codebread-1.0.5}/codebread/__init__.py +0 -0
  16. {codebread-1.0.4 → codebread-1.0.5}/codebread/classifier.py +0 -0
  17. {codebread-1.0.4 → codebread-1.0.5}/codebread/cli.py +0 -0
  18. {codebread-1.0.4 → codebread-1.0.5}/codebread/diff.py +0 -0
  19. {codebread-1.0.4 → codebread-1.0.5}/codebread/export.py +0 -0
  20. {codebread-1.0.4 → codebread-1.0.5}/codebread/languages.py +0 -0
  21. {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/__init__.py +0 -0
  22. {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/generic_parser.py +0 -0
  23. {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/javascript_parser.py +0 -0
  24. {codebread-1.0.4 → codebread-1.0.5}/codebread/parsers/python_parser.py +0 -0
  25. {codebread-1.0.4 → codebread-1.0.5}/codebread/scanner.py +0 -0
  26. {codebread-1.0.4 → codebread-1.0.5}/codebread/server.py +0 -0
  27. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/dependency_links.txt +0 -0
  28. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/entry_points.txt +0 -0
  29. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/requires.txt +0 -0
  30. {codebread-1.0.4 → codebread-1.0.5}/codebread.egg-info/top_level.txt +0 -0
  31. {codebread-1.0.4 → codebread-1.0.5}/setup.cfg +0 -0
  32. {codebread-1.0.4 → codebread-1.0.5}/tests/test_analyzer.py +0 -0
  33. {codebread-1.0.4 → codebread-1.0.5}/tests/test_config_parser.py +0 -0
  34. {codebread-1.0.4 → codebread-1.0.5}/tests/test_connections.py +0 -0
  35. {codebread-1.0.4 → codebread-1.0.5}/tests/test_generic_parser.py +0 -0
  36. {codebread-1.0.4 → codebread-1.0.5}/tests/test_javascript_parser.py +0 -0
  37. {codebread-1.0.4 → codebread-1.0.5}/tests/test_python_parser.py +0 -0
  38. {codebread-1.0.4 → codebread-1.0.5}/tests/test_scanner.py +0 -0
  39. {codebread-1.0.4 → codebread-1.0.5}/tests/test_server.py +0 -0
  40. {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.4
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}