cloak-cli 0.1.2__tar.gz → 0.2.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.
- cloak_cli-0.2.1/.pre-commit-hooks.yaml +35 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/CHANGELOG.md +15 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/PKG-INFO +29 -2
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/README.md +28 -1
- cloak_cli-0.2.1/examples/README.md +61 -0
- cloak_cli-0.2.1/examples/js-api-client/.cloakpolicy +25 -0
- cloak_cli-0.2.1/examples/js-api-client/client.js +34 -0
- cloak_cli-0.2.1/examples/python-pricing-engine/.cloakpolicy +24 -0
- cloak_cli-0.2.1/examples/python-pricing-engine/pricing.py +43 -0
- cloak_cli-0.2.1/examples/python-pricing-engine/test_pricing.py +19 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/pyproject.toml +1 -1
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/__init__.py +1 -1
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/cli.py +1 -1
- cloak_cli-0.2.1/src/cloak/obfuscate/js_transformer.py +175 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/obfuscate/runner.py +22 -3
- cloak_cli-0.2.1/tests/test_obfuscate_js.py +201 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/.cloakpolicy.example +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/.github/workflows/release.yml +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/.gitignore +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/CONTRIBUTING.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/LICENSE +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/AGENT_INTEGRATION.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/BUILD_PLAN.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/COMPETITOR_RESEARCH.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/PROMPTS.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/quotecraft.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/quotecraft.redacted.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/result_prompt1.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/docs/research/result_prompt2.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/__main__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/context/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/context/generator.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/context/js_redactor.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/filesystem.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/obfuscate/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/obfuscate/manifest.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/obfuscate/transformer.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/policy.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/policy_init.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/scan/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/src/cloak/scan/scanner.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/test_cli.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/test_context.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/test_context_js.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/test_obfuscate.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.1}/tests/test_policy_init.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# pre-commit hook definitions for CLOAK.
|
|
2
|
+
#
|
|
3
|
+
# Add to your .pre-commit-config.yaml:
|
|
4
|
+
#
|
|
5
|
+
# repos:
|
|
6
|
+
# - repo: https://github.com/newtophilly/cloak
|
|
7
|
+
# rev: v0.2.0
|
|
8
|
+
# hooks:
|
|
9
|
+
# - id: cloak-scan
|
|
10
|
+
#
|
|
11
|
+
# See https://pre-commit.com for general pre-commit setup.
|
|
12
|
+
|
|
13
|
+
- id: cloak-scan
|
|
14
|
+
name: cloak scan
|
|
15
|
+
description: >-
|
|
16
|
+
Find secrets and proprietary markers in code before commit. Wraps detect-secrets and
|
|
17
|
+
layers in policy.secret_rules from .cloakpolicy. Exits 1 on findings (blocks commit).
|
|
18
|
+
entry: cloak scan
|
|
19
|
+
language: python
|
|
20
|
+
pass_filenames: false
|
|
21
|
+
args: ["."]
|
|
22
|
+
always_run: true
|
|
23
|
+
stages: [pre-commit]
|
|
24
|
+
|
|
25
|
+
- id: cloak-context-preview
|
|
26
|
+
name: cloak context (preview only)
|
|
27
|
+
description: >-
|
|
28
|
+
Render a redacted markdown view of the repo. Useful as a manual check that the redaction
|
|
29
|
+
output looks right. Does not block commits — `stages: [manual]` means run only when you
|
|
30
|
+
invoke it explicitly: `pre-commit run cloak-context-preview --hook-stage manual`.
|
|
31
|
+
entry: cloak context
|
|
32
|
+
language: python
|
|
33
|
+
pass_filenames: false
|
|
34
|
+
args: [".", "--json"]
|
|
35
|
+
stages: [manual]
|
|
@@ -6,6 +6,21 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-05-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `.pre-commit-hooks.yaml` at repo root. Users can add `cloak-scan` to their `.pre-commit-config.yaml` and `cloak scan` runs as a pre-commit gate (exits 1 on findings, blocks the commit). A second `cloak-context-preview` hook is provided at `stages: [manual]` for opt-in invocation.
|
|
13
|
+
- `examples/` directory with two end-to-end demos: `python-pricing-engine/` (with pytest coverage so `--verify "pytest"` works) and `js-api-client/`. Each has its own `.cloakpolicy`. New users can `cd examples/python-pricing-engine && cloak scan .` and see CLOAK in action in 30 seconds. See `examples/README.md` for the walkthrough.
|
|
14
|
+
- README "Try it on the included examples" + "Use as a pre-commit hook" sections.
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2026-05-08
|
|
17
|
+
|
|
18
|
+
Feature matrix complete: all three headline commands now work for both Python and JS/TS.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Phase 5: `cloak obfuscate` now handles JavaScript and TypeScript files, dispatched by extension. Per-file rename of module-level identifiers starting with `_` (function declarations, class declarations, generator functions, `const`/`let`/`var` simple bindings) using tree-sitter for parsing and byte-splice for output. Skips dunder-like names (`__version`), all-underscore names, and anything matching `policy.public_api` (with trailing-`*` wildcard support). Property access (`obj._foo`), shorthand object properties, and destructuring patterns are deliberately not renamed in v1 — they would silently change object shapes and risk runtime breakage. Limitations are caught by `--verify` (cross-file imports, local-variable shadowing of module-level `_names`).
|
|
22
|
+
- `cloak obfuscate` against a mixed Python+JS repo now transforms both languages in one pass with one manifest, one rename map, and one verify command.
|
|
23
|
+
|
|
9
24
|
## [0.1.2] - 2026-05-08
|
|
10
25
|
|
|
11
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloak-cli
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Local CLI for safer LLM workflows: redact code before pasting, generate verified obfuscated copies, enforce policy from your repo.
|
|
5
5
|
Project-URL: Homepage, https://github.com/newtophilly/cloak
|
|
6
6
|
Project-URL: Repository, https://github.com/newtophilly/cloak
|
|
@@ -324,6 +324,33 @@ $ cloak obfuscate src/payments --out /tmp/payments.cloaked --verify "pytest test
|
|
|
324
324
|
$ cloak scan . --json # exits 1 if any secrets, JSON for parsing.
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
+
### Try it on the included examples
|
|
328
|
+
|
|
329
|
+
The repo ships [`examples/`](examples/) with one Python and one JS project, each with its own `.cloakpolicy`. Clone, install, and run end-to-end against either in 30 seconds:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
cd examples/python-pricing-engine
|
|
333
|
+
cloak scan .
|
|
334
|
+
cloak context . --copy
|
|
335
|
+
cloak obfuscate . --out /tmp/pricing.cloaked --verify "pytest"
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
See [examples/README.md](examples/README.md) for the full walkthrough.
|
|
339
|
+
|
|
340
|
+
### Use as a pre-commit hook
|
|
341
|
+
|
|
342
|
+
Drop `cloak scan` into `.pre-commit-config.yaml` to block commits that introduce secrets:
|
|
343
|
+
|
|
344
|
+
```yaml
|
|
345
|
+
repos:
|
|
346
|
+
- repo: https://github.com/newtophilly/cloak
|
|
347
|
+
rev: v0.2.0
|
|
348
|
+
hooks:
|
|
349
|
+
- id: cloak-scan
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Then `pre-commit install` and you're done. See [pre-commit.com](https://pre-commit.com) for general setup.
|
|
353
|
+
|
|
327
354
|
## How `.cloakpolicy` works
|
|
328
355
|
|
|
329
356
|
The policy lives in a `.cloakpolicy` YAML file at the repo root. It's checked into git, versioned with your code, and reviewed via the same PR process as everything else. Authority = whoever has merge access.
|
|
@@ -385,7 +412,7 @@ CLOAK is designed to be called as a subprocess from other developer tools and AI
|
|
|
385
412
|
| 3 | `cloak context` for Python | ✅ Done |
|
|
386
413
|
| 3.5 | `cloak context` JS/TS via tree-sitter | ✅ Done |
|
|
387
414
|
| 4 | `cloak obfuscate` Python with `--verify` | ✅ Done (v1) |
|
|
388
|
-
| 5 | `cloak obfuscate` JS/TS
|
|
415
|
+
| 5 | `cloak obfuscate` JS/TS via tree-sitter | ✅ Done (v1) |
|
|
389
416
|
| 6 | `cloak eval` (LLM-prompt-based regression harness) | ⏳ |
|
|
390
417
|
|
|
391
418
|
## Contributing
|
|
@@ -83,6 +83,33 @@ $ cloak obfuscate src/payments --out /tmp/payments.cloaked --verify "pytest test
|
|
|
83
83
|
$ cloak scan . --json # exits 1 if any secrets, JSON for parsing.
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Try it on the included examples
|
|
87
|
+
|
|
88
|
+
The repo ships [`examples/`](examples/) with one Python and one JS project, each with its own `.cloakpolicy`. Clone, install, and run end-to-end against either in 30 seconds:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd examples/python-pricing-engine
|
|
92
|
+
cloak scan .
|
|
93
|
+
cloak context . --copy
|
|
94
|
+
cloak obfuscate . --out /tmp/pricing.cloaked --verify "pytest"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [examples/README.md](examples/README.md) for the full walkthrough.
|
|
98
|
+
|
|
99
|
+
### Use as a pre-commit hook
|
|
100
|
+
|
|
101
|
+
Drop `cloak scan` into `.pre-commit-config.yaml` to block commits that introduce secrets:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
repos:
|
|
105
|
+
- repo: https://github.com/newtophilly/cloak
|
|
106
|
+
rev: v0.2.0
|
|
107
|
+
hooks:
|
|
108
|
+
- id: cloak-scan
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then `pre-commit install` and you're done. See [pre-commit.com](https://pre-commit.com) for general setup.
|
|
112
|
+
|
|
86
113
|
## How `.cloakpolicy` works
|
|
87
114
|
|
|
88
115
|
The policy lives in a `.cloakpolicy` YAML file at the repo root. It's checked into git, versioned with your code, and reviewed via the same PR process as everything else. Authority = whoever has merge access.
|
|
@@ -144,7 +171,7 @@ CLOAK is designed to be called as a subprocess from other developer tools and AI
|
|
|
144
171
|
| 3 | `cloak context` for Python | ✅ Done |
|
|
145
172
|
| 3.5 | `cloak context` JS/TS via tree-sitter | ✅ Done |
|
|
146
173
|
| 4 | `cloak obfuscate` Python with `--verify` | ✅ Done (v1) |
|
|
147
|
-
| 5 | `cloak obfuscate` JS/TS
|
|
174
|
+
| 5 | `cloak obfuscate` JS/TS via tree-sitter | ✅ Done (v1) |
|
|
148
175
|
| 6 | `cloak eval` (LLM-prompt-based regression harness) | ⏳ |
|
|
149
176
|
|
|
150
177
|
## Contributing
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# CLOAK examples
|
|
2
|
+
|
|
3
|
+
Two tiny projects to demonstrate `cloak scan`, `cloak context`, and `cloak obfuscate` end to end.
|
|
4
|
+
|
|
5
|
+
Both have their own `.cloakpolicy` so you can run CLOAK against them right away.
|
|
6
|
+
|
|
7
|
+
## `python-pricing-engine/`
|
|
8
|
+
|
|
9
|
+
A simplified pricing engine: a public `calculate_total` plus private `_apply_tier` and `_apply_region` helpers, with `_TIER_DISCOUNTS` and `_REGIONAL_MARKUPS` proprietary tables. Includes pytest coverage so `--verify` has something real to check.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd examples/python-pricing-engine
|
|
13
|
+
|
|
14
|
+
# 1. Scan for secrets / proprietary markers
|
|
15
|
+
cloak scan .
|
|
16
|
+
|
|
17
|
+
# 2. Generate redacted markdown safe to paste into ChatGPT/Claude
|
|
18
|
+
cloak context . --copy
|
|
19
|
+
# Bodies replaced with `...`. Tables (_TIER_DISCOUNTS, _REGIONAL_MARKUPS) replaced
|
|
20
|
+
# with `...`. Public API + signatures preserved.
|
|
21
|
+
|
|
22
|
+
# 3. Obfuscate, gated on the pytest suite passing
|
|
23
|
+
cloak obfuscate . --out /tmp/pricing.cloaked --verify "pytest"
|
|
24
|
+
# Private helpers renamed (_apply_tier → _a000, _apply_region → _a001).
|
|
25
|
+
# Public `calculate_total` preserved (it's in .cloakpolicy public_api).
|
|
26
|
+
# Tests pass against the obfuscated copy.
|
|
27
|
+
ls /tmp/pricing.cloaked/cloak-manifest.json # audit trail with hashes + rename map
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## `js-api-client/`
|
|
31
|
+
|
|
32
|
+
A small API client with a public `fetchJson` plus private `_buildHeaders` and `_normalizePath` helpers, and `_BASE_HEADERS` / `_TIMEOUT_MS` proprietary constants.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd examples/js-api-client
|
|
36
|
+
|
|
37
|
+
cloak scan .
|
|
38
|
+
cloak context . --copy
|
|
39
|
+
cloak obfuscate . --out /tmp/client.cloaked
|
|
40
|
+
# Note: no --verify here because the example doesn't ship a Node test runner setup.
|
|
41
|
+
# In your real project, you'd pass --verify "npm test" or similar.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What you should observe
|
|
45
|
+
|
|
46
|
+
- `cloak scan` exits 0 (no secrets in these examples) and prints a green "Clean" panel.
|
|
47
|
+
- `cloak context` produces markdown where signatures and class shapes survive, but bodies and proprietary tables are replaced with `...` (Python) or `/* [REDACTED BY CLOAK] */` (JS/TS).
|
|
48
|
+
- `cloak obfuscate` produces a transformed copy in the output dir with `_a000`/`_a001`/... identifiers replacing the original `_names`. Public-API names listed in `.cloakpolicy` are preserved.
|
|
49
|
+
- The `cloak-manifest.json` in the output dir records: cloak version, source/output sha256s, the rename map, the policy snapshot, and (if `--verify` was passed) the verify command + result.
|
|
50
|
+
|
|
51
|
+
## Want to try `--verify` failing?
|
|
52
|
+
|
|
53
|
+
Edit `pricing.py` to break the math (e.g., return `subtotal` instead of the discounted total) and rerun:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cloak obfuscate . --out /tmp/pricing.cloaked --verify "pytest"
|
|
57
|
+
# Exit code 1, panel shows the failing pytest output.
|
|
58
|
+
# Output is still written for inspection but the operation is reported as failed.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This is the differentiator: `cloak obfuscate` only succeeds if your tests pass against the transformed copy.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Example policy for the JS API client.
|
|
2
|
+
version: 1
|
|
3
|
+
|
|
4
|
+
sensitive_paths:
|
|
5
|
+
- "*.js"
|
|
6
|
+
- "*.ts"
|
|
7
|
+
|
|
8
|
+
# `fetchJson` is the public API — never let obfuscate rename it.
|
|
9
|
+
public_api:
|
|
10
|
+
- "fetchJson"
|
|
11
|
+
|
|
12
|
+
secret_rules: []
|
|
13
|
+
allow_strings: []
|
|
14
|
+
|
|
15
|
+
context_defaults:
|
|
16
|
+
keep_docstrings: true
|
|
17
|
+
redact_function_bodies: true
|
|
18
|
+
alias_enums: false
|
|
19
|
+
|
|
20
|
+
obfuscate_defaults:
|
|
21
|
+
rename_private: true
|
|
22
|
+
rename_public_api: false
|
|
23
|
+
encode_strings: false
|
|
24
|
+
strip_docstrings: false
|
|
25
|
+
profile: standard
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Tiny example API client — public function plus underscore-private helpers.
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates how `cloak context` and `cloak obfuscate` treat JS files.
|
|
4
|
+
|
|
5
|
+
const _BASE_HEADERS = {
|
|
6
|
+
"User-Agent": "example-client/1.0",
|
|
7
|
+
"Accept": "application/json",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const _TIMEOUT_MS = 5000;
|
|
11
|
+
|
|
12
|
+
function _buildHeaders(token) {
|
|
13
|
+
return {
|
|
14
|
+
..._BASE_HEADERS,
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _normalizePath(path) {
|
|
20
|
+
if (!path.startsWith("/")) return `/${path}`;
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fetchJson(baseUrl, path, token) {
|
|
25
|
+
const url = baseUrl + _normalizePath(path);
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
headers: _buildHeaders(token),
|
|
28
|
+
signal: AbortSignal.timeout(_TIMEOUT_MS),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`HTTP ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
return response.json();
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Example policy for the Python pricing engine.
|
|
2
|
+
version: 1
|
|
3
|
+
|
|
4
|
+
sensitive_paths:
|
|
5
|
+
- "*.py"
|
|
6
|
+
|
|
7
|
+
# `calculate_total` is the public entry point — never let obfuscate rename it.
|
|
8
|
+
public_api:
|
|
9
|
+
- "calculate_total"
|
|
10
|
+
|
|
11
|
+
secret_rules: []
|
|
12
|
+
allow_strings: []
|
|
13
|
+
|
|
14
|
+
context_defaults:
|
|
15
|
+
keep_docstrings: true
|
|
16
|
+
redact_function_bodies: true
|
|
17
|
+
alias_enums: false
|
|
18
|
+
|
|
19
|
+
obfuscate_defaults:
|
|
20
|
+
rename_private: true
|
|
21
|
+
rename_public_api: false
|
|
22
|
+
encode_strings: false
|
|
23
|
+
strip_docstrings: false
|
|
24
|
+
profile: standard
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tiny example pricing engine — public API + private helpers + 'proprietary tables'.
|
|
2
|
+
|
|
3
|
+
This file exists to demonstrate how `cloak context` and `cloak obfuscate` treat each kind
|
|
4
|
+
of definition. It is intentionally small and self-contained.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_TIER_DISCOUNTS = {
|
|
11
|
+
"basic": 0.00,
|
|
12
|
+
"pro": 0.10,
|
|
13
|
+
"enterprise": 0.20,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_REGIONAL_MARKUPS = {
|
|
17
|
+
"domestic": 1.00,
|
|
18
|
+
"international": 1.15,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Customer:
|
|
24
|
+
customer_id: str
|
|
25
|
+
tier: str
|
|
26
|
+
region: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def calculate_total(customer: Customer, subtotal: float) -> float:
|
|
30
|
+
"""Public API: produce a final price for a customer + subtotal."""
|
|
31
|
+
discount = _apply_tier(customer.tier)
|
|
32
|
+
markup = _apply_region(customer.region)
|
|
33
|
+
return subtotal * (1 - discount) * markup
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _apply_tier(tier: str) -> float:
|
|
37
|
+
"""Return the discount fraction for a tier."""
|
|
38
|
+
return _TIER_DISCOUNTS.get(tier, 0.0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _apply_region(region: str) -> float:
|
|
42
|
+
"""Return the price multiplier for a region."""
|
|
43
|
+
return _REGIONAL_MARKUPS.get(region, 1.0)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Tests for the example pricing engine — used by `cloak obfuscate --verify`."""
|
|
2
|
+
|
|
3
|
+
from pricing import Customer, calculate_total
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_basic_domestic() -> None:
|
|
7
|
+
c = Customer(customer_id="C1", tier="basic", region="domestic")
|
|
8
|
+
assert calculate_total(c, 100.0) == 100.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_pro_domestic_gets_10_percent() -> None:
|
|
12
|
+
c = Customer(customer_id="C2", tier="pro", region="domestic")
|
|
13
|
+
assert calculate_total(c, 100.0) == 90.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_enterprise_international() -> None:
|
|
17
|
+
c = Customer(customer_id="C3", tier="enterprise", region="international")
|
|
18
|
+
# 100 * 0.80 * 1.15 = 92.0
|
|
19
|
+
assert calculate_total(c, 100.0) == 92.0
|
|
@@ -247,7 +247,7 @@ def _emit_obfuscate_terminal(policy: Policy, result: ObfuscateResult) -> None:
|
|
|
247
247
|
"[bold green]✓ Obfuscated[/bold green]",
|
|
248
248
|
f"output: {result.output_dir}",
|
|
249
249
|
f"manifest: {result.manifest_path}",
|
|
250
|
-
f"transformed: {result.files_transformed}
|
|
250
|
+
f"transformed: {result.files_transformed} source files",
|
|
251
251
|
f"copied: {result.files_copied} other files",
|
|
252
252
|
f"renames: {len(result.rename_map)} module-private identifiers",
|
|
253
253
|
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Phase 5 — JS/TS obfuscation transformer.
|
|
2
|
+
|
|
3
|
+
Per-file rename of module-level identifiers that start with `_` (and aren't dunder-like or
|
|
4
|
+
all underscores). Same shape as the Python obfuscator: collect candidate names, build a
|
|
5
|
+
deterministic rename map (`_a000`, `_a001`, ...), then byte-splice every reference of those
|
|
6
|
+
names in the file with their new identifier.
|
|
7
|
+
|
|
8
|
+
Tree-sitter is used as a parser only — we don't unparse. Edits happen at the byte level so
|
|
9
|
+
all formatting and comments outside renamed identifiers survive.
|
|
10
|
+
|
|
11
|
+
v1 limitations (deferred — caught by `--verify`):
|
|
12
|
+
- Cross-file rename. If `import { _helper } from './lib'`, the import keeps `_helper` but
|
|
13
|
+
the target file renames it; tests will fail and the user knows immediately.
|
|
14
|
+
- Local-variable shadowing. If a function body defines `const _helper = ...` shadowing a
|
|
15
|
+
module-level `_helper`, both get renamed; tests will catch logic bugs that arise.
|
|
16
|
+
- Shorthand property in object literals (`{ _helper }`) and destructuring patterns
|
|
17
|
+
(`const { _helper } = ...`) are NOT renamed (would change object shape silently). This
|
|
18
|
+
means a module-level `_helper` referenced via shorthand goes un-renamed there.
|
|
19
|
+
- Class methods are NOT renamed (could be public API).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
from tree_sitter import Node, Parser
|
|
25
|
+
|
|
26
|
+
from cloak.context.js_redactor import JsLikeKind, _get_language
|
|
27
|
+
from cloak.policy import Policy
|
|
28
|
+
|
|
29
|
+
_RENAMEABLE_IDENTIFIER_TYPES = {"identifier", "type_identifier"}
|
|
30
|
+
|
|
31
|
+
_DEFINITION_NODE_TYPES = {
|
|
32
|
+
"function_declaration",
|
|
33
|
+
"generator_function_declaration",
|
|
34
|
+
"class_declaration",
|
|
35
|
+
"lexical_declaration",
|
|
36
|
+
"variable_declaration",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class JsTransformResult:
|
|
42
|
+
"""Result of transforming a single JS/TS file."""
|
|
43
|
+
|
|
44
|
+
source_text: str
|
|
45
|
+
output_text: str
|
|
46
|
+
rename_map: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transform_js_like_source(
|
|
50
|
+
source: str,
|
|
51
|
+
kind: JsLikeKind,
|
|
52
|
+
policy: Policy,
|
|
53
|
+
) -> JsTransformResult:
|
|
54
|
+
"""Apply v1 obfuscation to a JS/TS source string. Returns transformed text + rename map."""
|
|
55
|
+
src_bytes = source.encode("utf-8")
|
|
56
|
+
parser = Parser(_get_language(kind))
|
|
57
|
+
tree = parser.parse(src_bytes)
|
|
58
|
+
|
|
59
|
+
names = _collect_top_level_private_names(tree.root_node, src_bytes, policy)
|
|
60
|
+
rename_map = _build_rename_map(names)
|
|
61
|
+
|
|
62
|
+
if not rename_map:
|
|
63
|
+
return JsTransformResult(source_text=source, output_text=source)
|
|
64
|
+
|
|
65
|
+
edits: list[tuple[int, int, bytes]] = []
|
|
66
|
+
_collect_rename_edits(tree.root_node, src_bytes, rename_map, edits)
|
|
67
|
+
|
|
68
|
+
if not edits:
|
|
69
|
+
return JsTransformResult(source_text=source, output_text=source, rename_map=rename_map)
|
|
70
|
+
|
|
71
|
+
edits.sort(key=lambda e: e[0], reverse=True)
|
|
72
|
+
out = bytearray(src_bytes)
|
|
73
|
+
for start, end, replacement in edits:
|
|
74
|
+
out[start:end] = replacement
|
|
75
|
+
return JsTransformResult(
|
|
76
|
+
source_text=source,
|
|
77
|
+
output_text=out.decode("utf-8", errors="replace"),
|
|
78
|
+
rename_map=rename_map,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _collect_top_level_private_names(root_node: Node, src: bytes, policy: Policy) -> list[str]:
|
|
83
|
+
"""Find top-level definitions whose name starts with `_`. Skips dunders, all-underscore."""
|
|
84
|
+
names: list[str] = []
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
public_api = set(policy.public_api)
|
|
87
|
+
|
|
88
|
+
for child in root_node.children:
|
|
89
|
+
# `export ...` wraps a real declaration — peek through one level.
|
|
90
|
+
target = child
|
|
91
|
+
if child.type == "export_statement":
|
|
92
|
+
for inner in child.children:
|
|
93
|
+
if inner.type in _DEFINITION_NODE_TYPES:
|
|
94
|
+
target = inner
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
for name in _extract_definition_names(target, src):
|
|
98
|
+
if _should_rename(name, public_api) and name not in seen:
|
|
99
|
+
seen.add(name)
|
|
100
|
+
names.append(name)
|
|
101
|
+
|
|
102
|
+
return names
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _extract_definition_names(node: Node, src: bytes) -> list[str]:
|
|
106
|
+
"""Return the symbol names this top-level definition introduces."""
|
|
107
|
+
if node.type in {
|
|
108
|
+
"function_declaration",
|
|
109
|
+
"generator_function_declaration",
|
|
110
|
+
"class_declaration",
|
|
111
|
+
}:
|
|
112
|
+
name_node = node.child_by_field_name("name")
|
|
113
|
+
if name_node is not None:
|
|
114
|
+
return [_text(name_node, src)]
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
if node.type in {"lexical_declaration", "variable_declaration"}:
|
|
118
|
+
result: list[str] = []
|
|
119
|
+
for declarator in node.children:
|
|
120
|
+
if declarator.type != "variable_declarator":
|
|
121
|
+
continue
|
|
122
|
+
name_node = declarator.child_by_field_name("name")
|
|
123
|
+
# Only handle the simple `const _foo = ...` case. Destructuring patterns
|
|
124
|
+
# (object_pattern, array_pattern) are skipped — too risky to rename in v1.
|
|
125
|
+
if name_node is not None and name_node.type == "identifier":
|
|
126
|
+
result.append(_text(name_node, src))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _should_rename(name: str, public_api: set[str]) -> bool:
|
|
133
|
+
if not name.startswith("_"):
|
|
134
|
+
return False
|
|
135
|
+
if name.startswith("__"):
|
|
136
|
+
# Dunder-like; conservative skip (e.g. `__dirname` in CommonJS).
|
|
137
|
+
return False
|
|
138
|
+
if all(c == "_" for c in name):
|
|
139
|
+
return False
|
|
140
|
+
if name in public_api:
|
|
141
|
+
return False
|
|
142
|
+
for pattern in public_api:
|
|
143
|
+
if pattern.endswith("*") and name.startswith(pattern.rstrip("*")):
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_rename_map(names: list[str]) -> dict[str, str]:
|
|
149
|
+
return {name: f"_a{i:03d}" for i, name in enumerate(names)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _collect_rename_edits(
|
|
153
|
+
node: Node,
|
|
154
|
+
src: bytes,
|
|
155
|
+
rename_map: dict[str, str],
|
|
156
|
+
edits: list[tuple[int, int, bytes]],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Walk the tree; replace `identifier`/`type_identifier` nodes whose text is in the map.
|
|
159
|
+
|
|
160
|
+
Critically: we do NOT touch `property_identifier` (member access — `.foo` is a different
|
|
161
|
+
scope), `shorthand_property_identifier` (would silently change object shape), or
|
|
162
|
+
destructuring identifiers — see v1 limitations in module docstring.
|
|
163
|
+
"""
|
|
164
|
+
if node.type in _RENAMEABLE_IDENTIFIER_TYPES:
|
|
165
|
+
text = _text(node, src)
|
|
166
|
+
if text in rename_map:
|
|
167
|
+
edits.append((node.start_byte, node.end_byte, rename_map[text].encode("utf-8")))
|
|
168
|
+
return # leaf
|
|
169
|
+
|
|
170
|
+
for child in node.children:
|
|
171
|
+
_collect_rename_edits(child, src, rename_map, edits)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _text(node: Node, src: bytes) -> str:
|
|
175
|
+
return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
@@ -17,7 +17,9 @@ from dataclasses import dataclass, field
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
from cloak import __version__
|
|
20
|
+
from cloak.context.js_redactor import language_for_extension
|
|
20
21
|
from cloak.filesystem import walk_repo
|
|
22
|
+
from cloak.obfuscate.js_transformer import transform_js_like_source
|
|
21
23
|
from cloak.obfuscate.manifest import Manifest
|
|
22
24
|
from cloak.obfuscate.transformer import transform_python_source
|
|
23
25
|
from cloak.policy import Policy
|
|
@@ -81,6 +83,8 @@ def run_obfuscate(
|
|
|
81
83
|
|
|
82
84
|
source_hashes[str(rel)] = _sha256_file(src_file)
|
|
83
85
|
|
|
86
|
+
js_like_kind = language_for_extension(src_file.suffix)
|
|
87
|
+
|
|
84
88
|
if src_file.suffix == ".py":
|
|
85
89
|
try:
|
|
86
90
|
source_text = src_file.read_text(encoding="utf-8")
|
|
@@ -91,7 +95,7 @@ def run_obfuscate(
|
|
|
91
95
|
continue
|
|
92
96
|
|
|
93
97
|
try:
|
|
94
|
-
|
|
98
|
+
py_result = transform_python_source(source_text, policy, file_label=str(rel))
|
|
95
99
|
except SyntaxError:
|
|
96
100
|
# Don't break on un-parseable files. Copy them through unchanged.
|
|
97
101
|
shutil.copy2(src_file, out_file)
|
|
@@ -99,9 +103,24 @@ def run_obfuscate(
|
|
|
99
103
|
files_copied += 1
|
|
100
104
|
continue
|
|
101
105
|
|
|
102
|
-
out_file.write_text(
|
|
106
|
+
out_file.write_text(py_result.output_text, encoding="utf-8")
|
|
107
|
+
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
108
|
+
for orig, new in py_result.rename_map.items():
|
|
109
|
+
rename_map_global[f"{rel}:{orig}"] = new
|
|
110
|
+
files_transformed += 1
|
|
111
|
+
elif js_like_kind is not None:
|
|
112
|
+
try:
|
|
113
|
+
source_text = src_file.read_text(encoding="utf-8")
|
|
114
|
+
except (UnicodeDecodeError, OSError):
|
|
115
|
+
shutil.copy2(src_file, out_file)
|
|
116
|
+
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
117
|
+
files_copied += 1
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
js_result = transform_js_like_source(source_text, js_like_kind, policy)
|
|
121
|
+
out_file.write_text(js_result.output_text, encoding="utf-8")
|
|
103
122
|
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
104
|
-
for orig, new in
|
|
123
|
+
for orig, new in js_result.rename_map.items():
|
|
105
124
|
rename_map_global[f"{rel}:{orig}"] = new
|
|
106
125
|
files_transformed += 1
|
|
107
126
|
else:
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Tests for `cloak obfuscate` JS/TS support (Phase 5)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from cloak.cli import app
|
|
9
|
+
from cloak.obfuscate.js_transformer import transform_js_like_source
|
|
10
|
+
from cloak.obfuscate.runner import run_obfuscate
|
|
11
|
+
from cloak.policy import Policy
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------- transformer unit tests ----------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_renames_module_level_function() -> None:
|
|
20
|
+
src = (
|
|
21
|
+
"function _helper(x) {\n"
|
|
22
|
+
" return x + 1;\n"
|
|
23
|
+
"}\n"
|
|
24
|
+
"\n"
|
|
25
|
+
"export function publicFn(x) {\n"
|
|
26
|
+
" return _helper(x);\n"
|
|
27
|
+
"}\n"
|
|
28
|
+
)
|
|
29
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
30
|
+
assert "_helper" not in result.output_text
|
|
31
|
+
assert "_a000" in result.output_text
|
|
32
|
+
# Public name preserved
|
|
33
|
+
assert "publicFn" in result.output_text
|
|
34
|
+
# Reference inside publicFn correctly rewritten
|
|
35
|
+
assert "_a000(x)" in result.output_text
|
|
36
|
+
assert result.rename_map == {"_helper": "_a000"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_renames_module_level_const() -> None:
|
|
40
|
+
src = "const _internal = 42;\nexport const value = _internal * 2;\n"
|
|
41
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
42
|
+
assert "_internal" not in result.output_text
|
|
43
|
+
assert "_a000" in result.output_text
|
|
44
|
+
assert "value = _a000 * 2" in result.output_text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_renames_module_level_class() -> None:
|
|
48
|
+
src = (
|
|
49
|
+
"class _Internal {\n"
|
|
50
|
+
" constructor() { this.x = 1; }\n"
|
|
51
|
+
"}\n"
|
|
52
|
+
"\n"
|
|
53
|
+
"export function make() {\n"
|
|
54
|
+
" return new _Internal();\n"
|
|
55
|
+
"}\n"
|
|
56
|
+
)
|
|
57
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
58
|
+
assert "_Internal" not in result.output_text
|
|
59
|
+
assert "_a000" in result.output_text
|
|
60
|
+
assert "new _a000()" in result.output_text
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_does_not_rename_dunder_like_names() -> None:
|
|
64
|
+
src = "const __version = '1.0';\nconst _foo = 1;\n"
|
|
65
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
66
|
+
assert "__version" in result.output_text # preserved
|
|
67
|
+
assert "_foo" not in result.output_text # renamed
|
|
68
|
+
assert "_a000" in result.output_text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_respects_public_api_in_policy() -> None:
|
|
72
|
+
src = "export function _publicHelper(x) { return x; }\n"
|
|
73
|
+
policy = Policy(public_api=["_publicHelper"])
|
|
74
|
+
result = transform_js_like_source(src, "javascript", policy)
|
|
75
|
+
assert "_publicHelper" in result.output_text
|
|
76
|
+
assert not result.rename_map
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_handles_export_statement_with_lexical_declaration() -> None:
|
|
80
|
+
src = "export const _privateUtil = (x) => x * 2;\n"
|
|
81
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
82
|
+
# The name should still be renamed even though it's wrapped in `export`.
|
|
83
|
+
assert "_privateUtil" not in result.output_text
|
|
84
|
+
assert "_a000" in result.output_text
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_preserves_property_access_with_same_name() -> None:
|
|
88
|
+
"""obj._helper is a member access — different scope; we don't rename it."""
|
|
89
|
+
src = (
|
|
90
|
+
"function _helper(x) { return x * 2; }\n"
|
|
91
|
+
"const obj = { _helper: 'something else' };\n"
|
|
92
|
+
"export function go() {\n"
|
|
93
|
+
" return _helper(obj._helper);\n"
|
|
94
|
+
"}\n"
|
|
95
|
+
)
|
|
96
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
97
|
+
# The bare `_helper` reference (function call) should be renamed.
|
|
98
|
+
# The property access `obj._helper` should NOT be renamed (different scope).
|
|
99
|
+
assert "obj._helper" in result.output_text
|
|
100
|
+
# The standalone calls should be renamed.
|
|
101
|
+
assert "_a000(obj._helper)" in result.output_text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_typescript_renames_top_level_types() -> None:
|
|
105
|
+
"""TypeScript: rename top-level _-prefixed type identifiers as well."""
|
|
106
|
+
src = (
|
|
107
|
+
"type _Internal = { x: number };\nexport function make(): _Internal { return { x: 1 }; }\n"
|
|
108
|
+
)
|
|
109
|
+
result = transform_js_like_source(src, "typescript", Policy())
|
|
110
|
+
# type_alias_declaration: tree-sitter-typescript should expose this; for v1, our
|
|
111
|
+
# _DEFINITION_NODE_TYPES doesn't include it, so the rename won't happen. This is
|
|
112
|
+
# documented as a limitation. The point of this test: confirm we don't crash.
|
|
113
|
+
assert result.output_text # non-empty
|
|
114
|
+
# The visible behavior may or may not rename; either is acceptable for v1.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_returns_unchanged_when_no_private_names() -> None:
|
|
118
|
+
src = "export function pub(x) { return x; }\n"
|
|
119
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
120
|
+
assert result.output_text == src
|
|
121
|
+
assert not result.rename_map
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_handles_invalid_source_gracefully() -> None:
|
|
125
|
+
src = "function broken( {{{"
|
|
126
|
+
# Should not raise.
|
|
127
|
+
transform_js_like_source(src, "javascript", Policy())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------- pipeline integration ----------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _make_js_repo(tmp_path: Path) -> Path:
|
|
134
|
+
"""Sample JS repo: lib + matching test that imports the public function."""
|
|
135
|
+
repo = tmp_path / "src_repo"
|
|
136
|
+
repo.mkdir()
|
|
137
|
+
(repo / "lib.js").write_text(
|
|
138
|
+
"function _double(x) {\n"
|
|
139
|
+
" return x * 2;\n"
|
|
140
|
+
"}\n"
|
|
141
|
+
"\n"
|
|
142
|
+
"export function publicFn(x) {\n"
|
|
143
|
+
" return _double(x);\n"
|
|
144
|
+
"}\n"
|
|
145
|
+
)
|
|
146
|
+
(repo / "package.json").write_text('{"type":"module"}\n')
|
|
147
|
+
return repo
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_runner_handles_mixed_python_and_js(tmp_path: Path) -> None:
|
|
151
|
+
repo = tmp_path / "repo"
|
|
152
|
+
repo.mkdir()
|
|
153
|
+
(repo / "lib.py").write_text(
|
|
154
|
+
"def _helper(x):\n return x + 1\n\ndef pub(x):\n return _helper(x)\n"
|
|
155
|
+
)
|
|
156
|
+
(repo / "lib.js").write_text(
|
|
157
|
+
"function _helper(x) { return x + 1; }\nexport function pub(x) { return _helper(x); }\n"
|
|
158
|
+
)
|
|
159
|
+
out = tmp_path / "obfuscated"
|
|
160
|
+
|
|
161
|
+
result = run_obfuscate(repo, out, Policy())
|
|
162
|
+
assert result.files_transformed == 2 # both py and js were transformed
|
|
163
|
+
py_text = (out / "lib.py").read_text()
|
|
164
|
+
js_text = (out / "lib.js").read_text()
|
|
165
|
+
assert "_helper" not in py_text
|
|
166
|
+
assert "_helper" not in js_text
|
|
167
|
+
assert "_a000" in py_text
|
|
168
|
+
assert "_a000" in js_text
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_runner_writes_manifest_with_js_renames(tmp_path: Path) -> None:
|
|
172
|
+
repo = _make_js_repo(tmp_path)
|
|
173
|
+
out = tmp_path / "obfuscated"
|
|
174
|
+
run_obfuscate(repo, out, Policy())
|
|
175
|
+
|
|
176
|
+
manifest = json.loads((out / "cloak-manifest.json").read_text())
|
|
177
|
+
assert "lib.js:_double" in manifest["rename_map"]
|
|
178
|
+
assert manifest["rename_map"]["lib.js:_double"] == "_a000"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------- CLI integration ----------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_cli_obfuscate_against_js_repo(tmp_path: Path) -> None:
|
|
185
|
+
repo = _make_js_repo(tmp_path)
|
|
186
|
+
out = tmp_path / "obf"
|
|
187
|
+
result = runner.invoke(app, ["obfuscate", str(repo), "--out", str(out)])
|
|
188
|
+
assert result.exit_code == 0, result.stdout
|
|
189
|
+
obfuscated = (out / "lib.js").read_text()
|
|
190
|
+
assert "_double" not in obfuscated
|
|
191
|
+
assert "_a000" in obfuscated
|
|
192
|
+
assert "publicFn" in obfuscated # public preserved
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_cli_obfuscate_panel_shows_renames(tmp_path: Path) -> None:
|
|
196
|
+
repo = _make_js_repo(tmp_path)
|
|
197
|
+
out = tmp_path / "obf"
|
|
198
|
+
result = runner.invoke(app, ["obfuscate", str(repo), "--out", str(out)])
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
assert "Obfuscated" in result.stdout
|
|
201
|
+
assert "1 module-private" in result.stdout or "renames:" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|