anonproxy 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 (50) hide show
  1. anonproxy-0.1.1/LICENSE +21 -0
  2. anonproxy-0.1.1/PKG-INFO +544 -0
  3. anonproxy-0.1.1/README.md +514 -0
  4. anonproxy-0.1.1/anonproxy/__init__.py +22 -0
  5. anonproxy-0.1.1/anonproxy/__main__.py +4 -0
  6. anonproxy-0.1.1/anonproxy/audit.py +195 -0
  7. anonproxy-0.1.1/anonproxy/cli.py +365 -0
  8. anonproxy-0.1.1/anonproxy/config.py +156 -0
  9. anonproxy-0.1.1/anonproxy/detectors/__init__.py +145 -0
  10. anonproxy-0.1.1/anonproxy/detectors/anonymizer_slm.py +119 -0
  11. anonproxy-0.1.1/anonproxy/detectors/gliner2_detector.py +89 -0
  12. anonproxy-0.1.1/anonproxy/detectors/gliner_detector.py +76 -0
  13. anonproxy-0.1.1/anonproxy/detectors/llm_detector.py +219 -0
  14. anonproxy-0.1.1/anonproxy/detectors/openai_pii_detector.py +82 -0
  15. anonproxy-0.1.1/anonproxy/detectors/piiranha_detector.py +83 -0
  16. anonproxy-0.1.1/anonproxy/detectors/regex_detector.py +190 -0
  17. anonproxy-0.1.1/anonproxy/detectors/scope_detector.py +98 -0
  18. anonproxy-0.1.1/anonproxy/engine.py +253 -0
  19. anonproxy-0.1.1/anonproxy/profiles.py +228 -0
  20. anonproxy-0.1.1/anonproxy/proxy/__init__.py +1 -0
  21. anonproxy-0.1.1/anonproxy/proxy/app.py +296 -0
  22. anonproxy-0.1.1/anonproxy/proxy/streaming.py +137 -0
  23. anonproxy-0.1.1/anonproxy/proxy/transform.py +115 -0
  24. anonproxy-0.1.1/anonproxy/restorer.py +197 -0
  25. anonproxy-0.1.1/anonproxy/surrogates.py +279 -0
  26. anonproxy-0.1.1/anonproxy/vault.py +165 -0
  27. anonproxy-0.1.1/anonproxy/verify.py +273 -0
  28. anonproxy-0.1.1/anonproxy/wizard.py +226 -0
  29. anonproxy-0.1.1/anonproxy.egg-info/PKG-INFO +544 -0
  30. anonproxy-0.1.1/anonproxy.egg-info/SOURCES.txt +48 -0
  31. anonproxy-0.1.1/anonproxy.egg-info/dependency_links.txt +1 -0
  32. anonproxy-0.1.1/anonproxy.egg-info/entry_points.txt +2 -0
  33. anonproxy-0.1.1/anonproxy.egg-info/requires.txt +26 -0
  34. anonproxy-0.1.1/anonproxy.egg-info/top_level.txt +1 -0
  35. anonproxy-0.1.1/pyproject.toml +32 -0
  36. anonproxy-0.1.1/setup.cfg +4 -0
  37. anonproxy-0.1.1/tests/test_audit.py +91 -0
  38. anonproxy-0.1.1/tests/test_config.py +33 -0
  39. anonproxy-0.1.1/tests/test_detectors.py +119 -0
  40. anonproxy-0.1.1/tests/test_llm_detector.py +55 -0
  41. anonproxy-0.1.1/tests/test_polish.py +174 -0
  42. anonproxy-0.1.1/tests/test_profiles.py +122 -0
  43. anonproxy-0.1.1/tests/test_proxy.py +268 -0
  44. anonproxy-0.1.1/tests/test_roundtrip.py +127 -0
  45. anonproxy-0.1.1/tests/test_scope.py +53 -0
  46. anonproxy-0.1.1/tests/test_streaming.py +50 -0
  47. anonproxy-0.1.1/tests/test_tool_calls.py +204 -0
  48. anonproxy-0.1.1/tests/test_vault.py +28 -0
  49. anonproxy-0.1.1/tests/test_verify.py +73 -0
  50. anonproxy-0.1.1/tests/test_webapp.py +55 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nethemba
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,544 @@
1
+ Metadata-Version: 2.4
2
+ Name: anonproxy
3
+ Version: 0.1.1
4
+ Summary: Reversible anonymization proxy for sending pentest data to LLMs
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: uvicorn[standard]>=0.27
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7; extra == "dev"
14
+ Provides-Extra: gliner
15
+ Requires-Dist: gliner>=0.2.13; extra == "gliner"
16
+ Provides-Extra: gliner2
17
+ Requires-Dist: gliner2>=0.1; extra == "gliner2"
18
+ Provides-Extra: piiranha
19
+ Requires-Dist: transformers>=4.40; extra == "piiranha"
20
+ Requires-Dist: torch>=2.2; extra == "piiranha"
21
+ Provides-Extra: openai-pii
22
+ Requires-Dist: transformers>=4.40; extra == "openai-pii"
23
+ Requires-Dist: torch>=2.2; extra == "openai-pii"
24
+ Provides-Extra: all
25
+ Requires-Dist: gliner>=0.2.13; extra == "all"
26
+ Requires-Dist: gliner2>=0.1; extra == "all"
27
+ Requires-Dist: transformers>=4.40; extra == "all"
28
+ Requires-Dist: torch>=2.2; extra == "all"
29
+ Dynamic: license-file
30
+
31
+ # Anonproxy
32
+
33
+ **A reversible anonymization layer for sending pentest data to LLMs — built to be
34
+ more reliable than match/replace.**
35
+
36
+ Anonproxy sits between your tools (Claude Code, the OpenAI SDK, Burp Suite) and
37
+ the LLM API. It strips IPs, hostnames, credentials, hashes, tokens, org names and
38
+ PII out of everything *before* it leaves the machine, and restores the real
39
+ values in the reply. The model reasons over realistic surrogates; you read real
40
+ data.
41
+
42
+ It is a ground-up reimplementation inspired by
43
+ [`DontFeedTheAI`](https://github.com/zeroc00I/DontFeedTheAI), focused on the part
44
+ that was only working ~75% of the time: **the round trip**.
45
+
46
+ > **In plain terms:** you run a small program on your laptop. Your AI tools talk
47
+ > to it instead of talking to the cloud directly. It swaps client data for
48
+ > realistic fakes before anything leaves your machine, and swaps the real values
49
+ > back into the answer. The cloud AI never sees real client data; you never see
50
+ > the fakes.
51
+ >
52
+ > **New here? Read [QUICKSTART.md](QUICKSTART.md) first** — three steps, no jargon.
53
+
54
+ ---
55
+
56
+ ## Why the old approach hit ~75%
57
+
58
+ Match/replace (and the original's exact-substring restore) breaks the moment the
59
+ model touches a surrogate. Given a surrogate `host-ab12cd9`, models routinely
60
+ write it back as:
61
+
62
+ | What the model writes | Exact `str.replace` restores? |
63
+ |---|:---:|
64
+ | `host-ab12cd9` | ✅ |
65
+ | `**host-ab12cd9**` (bold) | ✅ (wrapper is outside) |
66
+ | `` `host-ab12cd9` `` (inline code) | ✅ |
67
+ | `HOST-AB12CD9` (case changed) | ❌ |
68
+ | `` `host-`ab12cd9 `` (emphasis *inside* the token) | ❌ |
69
+ | `host‑ab12cd9` (non-breaking hyphen / line wrap) | ❌ |
70
+ | surrogate split across two streaming chunks | ❌ |
71
+
72
+ Add detection gaps (regex can't see a bare hostname) and a small local model's
73
+ inconsistency, and you land around three-quarters. Our reproducible benchmark
74
+ puts naive exact-replace at **78%** and Anonproxy's restorer at **100%** on the
75
+ same mangling patterns:
76
+
77
+ ```
78
+ $ python3 scripts/benchmark_roundtrip.py
79
+
80
+ mangling naive str.replace tolerant restorer
81
+ ------------------------------------------------------
82
+ verbatim 7/ 7 ( 100%) 7/7 ( 100%)
83
+ uppercase 2/ 7 ( 29%) 7/7 ( 100%)
84
+ lowercase 6/ 7 ( 86%) 7/7 ( 100%)
85
+ intra_code 0/ 7 ( 0%) 7/7 ( 100%)
86
+ bold_segment 3/ 7 ( 43%) 7/7 ( 100%)
87
+ ...
88
+ OVERALL 60/ 77 ( 78%) 77/77 ( 100%)
89
+ ```
90
+
91
+ ## What makes it more reliable
92
+
93
+ **1. Tolerant restoration.** Restoration matches a surrogate against a normalized
94
+ projection of the reply (markdown noise *inside* the token removed, whitespace
95
+ collapsed, case folded, unicode hyphens normalized) while keeping an index map
96
+ back to the original text, so it recovers the value even when the model
97
+ reformats it. It deliberately does **not** expand outward to eat markdown
98
+ *around* a matched span (e.g. `**`/backticks hugging it) — those may be genuine
99
+ original content rather than model-added formatting, and swallowing them
100
+ corrupted real pages that happened to use `*emphasis*` near a redacted value.
101
+ (`anonproxy/restorer.py`)
102
+
103
+ **2. Streaming-safe.** A per-content-block hold-back buffer reassembles a
104
+ surrogate split across SSE deltas before restoring it, so you get real values
105
+ *as the response streams*, not after a full buffer. (`anonproxy/proxy/streaming.py`)
106
+
107
+ **3. Consistency by construction.** Surrogates are deterministic (HMAC keyed on
108
+ the engagement id) *and* vault-backed, so the same original always maps to the
109
+ same surrogate on an exact re-sighting — even across restarts or a lost vault.
110
+ A consistency rescan re-detects anything the vault has ever seen, so an entity
111
+ caught once is caught every time. (Two *different* casings of the same
112
+ real-world value, e.g. `WordPress.org` and `wordpress.org` both appearing in
113
+ one page, are treated as distinct sightings with their own surrogates — the
114
+ alternative, collapsing them, can only remember one original spelling and gets
115
+ the other one's restoration wrong.) (`anonproxy/surrogates.py`, `anonproxy/vault.py`)
116
+
117
+ **4. Format-preserving surrogates.** A hash surrogate is hex of the same length;
118
+ an AWS key keeps its `AKIA` prefix; an IP is a valid RFC 5737 TEST-NET address; a
119
+ payment card stays 16 Luhn-valid digits with the same grouping. The model treats
120
+ them as the real thing and has no reason to "correct" them — which also *prevents*
121
+ mangling in the first place. Precise floor spans also win over a broader model
122
+ span that merely wraps them, so structure like a `PHPSESSID=` cookie name is kept
123
+ while the value is swapped.
124
+
125
+ **5. Layered, pluggable detection.** A deterministic regex floor (IPs, hashes,
126
+ JWTs, cloud keys, FQDNs, MACs) plus a *configurable chain* of contextual backends
127
+ and the consistency rescan. Contextual backends are additive: if one is down,
128
+ regex + rescan carry on. See [Detection backends](#detection-backends).
129
+ (`anonproxy/detectors/`)
130
+
131
+ ## Architecture
132
+
133
+ ```
134
+ real data surrogates only
135
+ client ───────────────▶ Anonproxy ───────────────────▶ LLM API
136
+ (Claude Code / (engine + (Anthropic /
137
+ OpenAI SDK / Burp) proxy) ◀─────────────────── OpenAI-compatible)
138
+ ▲ response w/ surrogates
139
+ │ restored (tolerant, streaming)
140
+
141
+ per-engagement vault (SQLite, isolated)
142
+ ```
143
+
144
+ One engine, three front ends:
145
+
146
+ * **Library** — `from anonproxy import Engine`
147
+ * **Proxy** — `python -m anonproxy serve` (Anthropic + OpenAI shapes)
148
+ * **Burp extension** — `burp/` delegates to the engine's local API, so Burp
149
+ shares the same vault and tolerant restorer (see `burp/README.md`)
150
+
151
+ ## Quick start (all local)
152
+
153
+ Everything runs on your machine — no VPS, no remote, nothing sensitive leaves
154
+ the host. Pick whichever setup you prefer.
155
+
156
+ **A. macOS app (recommended)**
157
+
158
+ Download `Anonbar-macos.zip` from the project's
159
+ [Releases](../../releases) page, unzip, drag **Anonbar.app** to
160
+ Applications, and open it (unsigned local build: right-click → *Open* the
161
+ first time, or `xattr -cr /Applications/Anonbar.app`). The 🛡️ icon appears
162
+ in your menu bar.
163
+
164
+ Requirements: any Python ≥ 3.10 on the machine (`brew install python@3.12`).
165
+ Nothing else — on first **Start** the app creates its own isolated
166
+ environment (`~/.anonproxy/venv`) and installs its dependencies for you.
167
+ Building from source instead? See
168
+ [Fast workflow](#fast-workflow-profiles-scripts_anon-macos-menubar).
169
+
170
+ **B. Guided wizard**
171
+
172
+ ```bash
173
+ # one-time install (Python 3.10+): gives you the `anonproxy` command
174
+ pipx install anonproxy # or: pip install anonproxy / pip install .
175
+ anonproxy wizard
176
+ ```
177
+
178
+ From source instead?
179
+
180
+ ```bash
181
+ git clone https://github.com/jfreemansh/Anonproxy && cd Anonproxy
182
+ pip install . # or: pip install -r requirements.txt + python -m anonproxy
183
+ python -m anonproxy wizard
184
+ ```
185
+
186
+ It asks for the engagement name, finds/pulls an Ollama model, writes a `.env`,
187
+ and offers to launch the proxy.
188
+
189
+ **C. Manual**
190
+
191
+ ```bash
192
+ pip install -r requirements.txt
193
+ ollama pull qwen3:4b # optional, for best recall
194
+ python -m anonproxy serve --engagement acme-2026
195
+ ```
196
+
197
+ **D. Docker (bundles Ollama)**
198
+
199
+ ```bash
200
+ ENGAGEMENT_ID=acme-2026 docker compose up -d
201
+ docker compose exec ollama ollama pull qwen3:4b # one-time
202
+ ```
203
+
204
+ Then point your client at it:
205
+
206
+ ```bash
207
+ # Claude Code
208
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8099
209
+ claude
210
+
211
+ # OpenAI SDK / OpenRouter
212
+ # base_url = "http://127.0.0.1:8099/v1"
213
+ ```
214
+
215
+ ### Fast workflow: profiles (`scripts/anon`, macOS menubar)
216
+
217
+ > **The proxy listens on 8099, not 8080** — Burp's upstream proxy owns 8080,
218
+ > and this tool ships a Burp extension that talks to a *running* Anonproxy.
219
+ > Keeping the two apart means both are up at once with zero configuration.
220
+
221
+ One JSON profile per client/test carries the whole context — engagement id
222
+ (→ vault isolation), scope seed, detector chain, model, port. Spin-up between
223
+ tests is one command instead of re-typed flags:
224
+
225
+ ```bash
226
+ scripts/anon profile new acme-web --scope "acme.com,portal.acme.com,DC01" --notes "web app test"
227
+ scripts/anon up acme-web --daemon # detached; logs under ~/.anonproxy/logs/
228
+ scripts/anon env acme-web --copy # client export lines → clipboard
229
+ scripts/anon stop # reap the daemon
230
+ scripts/anon close acme-web # export JSON+CSV evidence + wipe vault
231
+ ```
232
+
233
+ `profile list|show|edit|rm` round it out (profiles live in
234
+ `~/.anonproxy/profiles/`; `ANONPROXY_PROFILE_DIR` overrides). `up <profile>`
235
+ without `--daemon` behaves like `serve` with the profile applied.
236
+
237
+ On macOS there's also a **native status-bar app** (AppKit — no Python UI deps,
238
+ no app bundle required): pick an engagement, Start/Stop, + **New engagement…**
239
+ (a real form: name, scope terms, port, notes, ephemeral toggle), copy client
240
+ env, open `/audit`, run `verify`, and one-click **Export & archive vault…**
241
+ close-out.
242
+
243
+ ```bash
244
+ scripts/install_anonbar.sh # one-time: builds + installs /Applications/Anonbar.app
245
+ open -a Anonbar # or Spotlight: ⌘Space → "Anonbar"
246
+ ```
247
+
248
+ Rebuild/reinstall only when `anonbar.swift` itself changes. For auto-start at
249
+ login: System Settings → General → Login Items → add Anonbar.
250
+
251
+ It shells out to the same CLI, so profiles created in either place just show
252
+ up in the other. Env overrides: `ANONPROXY_HOME`, `PYTHON_BIN`.
253
+
254
+ > Ephemeral profiles (`--ephemeral`) leave nothing on disk — but that also
255
+ > means a close-out *after* stopping finds no mappings. Run close-out while
256
+ > the proxy still holds them, or keep persistence on when evidence matters.
257
+
258
+ Quick offline check without a client:
259
+
260
+ ```bash
261
+ echo 'Host dc01.acmecorp.local at 10.20.0.10, NTLM 8846f7eaee8fb117ad06bdd830b7586c' \
262
+ | python -m anonproxy anon --engagement acme-2026
263
+ ```
264
+
265
+ ## End-to-end example (no Burp)
266
+
267
+ Send real tool output to a real LLM through the proxy and get a useful answer
268
+ back — while the API only ever sees fake data.
269
+
270
+ ```bash
271
+ # 1. start the proxy
272
+ python -m anonproxy serve --engagement acme-2026 &
273
+
274
+ # 2. run a tool, then ask Claude about it THROUGH the proxy
275
+ SCAN=$(nmap -sV dc01.acmecorp.local)
276
+
277
+ curl -s http://127.0.0.1:8099/v1/messages \
278
+ -H "x-api-key: $ANTHROPIC_API_KEY" \
279
+ -H "anthropic-version: 2023-06-01" \
280
+ -H "content-type: application/json" \
281
+ -d "$(jq -n --arg scan "$SCAN" '{
282
+ model: "claude-sonnet-4-6",
283
+ max_tokens: 1024,
284
+ messages: [{role:"user", content:("Analyse this scan and suggest next steps:\n" + $scan)}]
285
+ }')"
286
+ ```
287
+
288
+ What just happened:
289
+
290
+ - **Anthropic only saw surrogates** — e.g. `203.0.113.47` and
291
+ `host-ab12cd9.pentest.local`, never `10.20.0.10` or `dc01.acmecorp.local`.
292
+ - **Your reply has the real values back** — even if the model wrote them in bold
293
+ or changed their case.
294
+ - Open `http://127.0.0.1:8099/audit` to see exactly what was swapped.
295
+
296
+ The same works for OpenAI-style clients — just call
297
+ `http://127.0.0.1:8099/v1/chat/completions` with your usual OpenAI payload.
298
+
299
+ ## Audit dashboard
300
+
301
+ Open `http://127.0.0.1:8099/audit` (or `python -m anonproxy audit`) to review
302
+ every `original → surrogate` mapping live during an engagement — filterable by
303
+ type, with counts and CSV export. It binds to localhost and honours
304
+ `ANONPROXY_API_TOKEN` if set; disable it with `ANONPROXY_AUDIT=false`. It exposes
305
+ the reverse lookup, so treat it as an operator-only debug view.
306
+
307
+ If a detector backend errors mid-engagement (a floor-detector crash, a contextual
308
+ backend throwing), the stats bar shows a red `⚠ <name> failed ×N` pill instead of
309
+ failing silently into a log line — check it if coverage looks off partway through
310
+ a session.
311
+
312
+ ## Engagement workflow
313
+
314
+ 1. **One engagement id per client** (`--engagement acme-2026`). This isolates the
315
+ vault so surrogates never cross between clients.
316
+ 2. Run the proxy locally on your testing machine. Everything — proxy, vault, and
317
+ the Ollama detector — stays on the host.
318
+ 3. Work normally — every request is anonymized, every reply restored. Watch
319
+ coverage live at `/audit`.
320
+ 4. At session close, `python -m anonproxy export --engagement acme-2026` (or the
321
+ audit page's CSV export) dumps the full `original → surrogate` map for your
322
+ evidence/audit trail, then archive or delete the vault. Use
323
+ `ANONPROXY_EPHEMERAL=1` for in-memory-only (no disk persistence).
324
+
325
+ ## Configuration
326
+
327
+ | Variable | Default | Description |
328
+ |---|---|---|
329
+ | `ENGAGEMENT_ID` | `default` | **Change per client.** Isolates the vault. |
330
+ | `ANONPROXY_SCOPE` | *(empty)* | Comma list of client domains/hostnames/orgs always anonymized. |
331
+ | `ANONPROXY_SCOPE_FILE` | *(empty)* | File of scope terms (one per line, optional `value=TYPE`). |
332
+ | `ANONPROXY_CONTEXTUAL_MIN_LEN` | `4` | Contextual single-token findings shorter than this are dropped as noise (`db`, `sql`). Scope terms and regex matches bypass it. |
333
+ | `ANONPROXY_DETECTORS` | `regex,ollama` | Backend chain (see [Detection backends](#detection-backends)). |
334
+ | `LLM_ENABLED` | `true` | `false` = drop the Ollama backend. |
335
+ | `OLLAMA_HOST` | `http://localhost:11434` | Ollama endpoint. |
336
+ | `OLLAMA_MODEL` | `qwen3:4b` | Local detection model. Any Ollama model works (`--model`). |
337
+ | `GLINER_MODEL` | `urchade/gliner_multi_pii-v1` | GLiNER model (if enabled). |
338
+ | `PIIRANHA_MODEL` | `iiiorg/piiranha-v1-detect-personal-information` | Piiranha model (if enabled). |
339
+ | `ANONYMIZER_SLM_MODEL` | `anonymizer-slm` | Ollama name of the imported Anonymizer SLM. |
340
+ | `ANONPROXY_TOLERANT` | `true` | Tolerant restoration (vs. exact). |
341
+ | `ANONPROXY_EPHEMERAL` | `false` | In-memory vault, nothing on disk. |
342
+ | `ANONPROXY_AUDIT` | `true` | Serve the `/audit` dashboard. |
343
+ | `ANONPROXY_PROFILE_DIR` | `~/.anonproxy/profiles` | Where engagement profiles (one JSON each) live. |
344
+ | `ANONPROXY_EXPORTS_DIR` | `~/.anonproxy/exports` | Close-out evidence output root. |
345
+ | `PORT` / `HOST` | `8099` / `127.0.0.1` | Proxy listen address. |
346
+ | `ANTHROPIC_UPSTREAM` | `https://api.anthropic.com` | Anthropic upstream. |
347
+ | `OPENAI_UPSTREAM` | `https://api.openai.com` | Any OpenAI-compatible endpoint — OpenRouter, Groq, Together, etc. Paste the provider's documented `base_url` as-is, with or without a trailing `/v1`; both work (`https://openrouter.ai/api/v1` and `https://openrouter.ai/api` are equivalent here). |
348
+ | `ANONPROXY_API_TOKEN` | *(empty)* | Require `X-Anonproxy-Token` on the engine API. **Empty means the engine API and `/audit` are unauthenticated** — fine on an isolated laptop bound to `127.0.0.1`, but set this if the proxy is ever reachable by anything else (a shared box, a tunneled VPS). |
349
+ | `ANONPROXY_STRICT` | `false` | Fail closed (502) instead of forwarding a request unredacted when its body can't be parsed as JSON. Off by default so odd/legacy clients keep working. |
350
+
351
+ ## Detection backends
352
+
353
+ Detection is a configurable chain set by `ANONPROXY_DETECTORS` (the wizard asks
354
+ too). The regex floor is always on and always first; everything else is
355
+ optional, lazily loaded, and skipped with a warning if its dependency or model
356
+ isn't present — so the default never breaks. Pick the trade-off your colleague
357
+ wants:
358
+
359
+ | Backend | Catches | Cost / setup |
360
+ |---|---|---|
361
+ | `regex` *(always on)* | IPs, CIDRs, hashes, JWTs, cloud keys, MACs, FQDNs, emails, labelled creds, payment cards (Luhn), Bearer/Basic auth, SSNs, and **any cookie value in a `Set-Cookie:`/`Cookie:` header — by structure, not by a fixed name list**, so a CMS's own custom session-cookie name (WordPress's `wordpress_logged_in_<hash>`, say) is caught the same as `PHPSESSID`. Also resilient to URL-encoded delimiters (`%7C` etc.) that would otherwise glue an alnum byte onto the next token and defeat a boundary-anchored match — a real, previously-silent gap on form-urlencoded bodies and cookie headers. | none — deterministic floor |
362
+ | `ollama` *(default)* | bare hostnames, org/project/person names, unlabelled creds in prose | local Ollama + a model (`--model`) |
363
+ | `gliner2` ⭐ *(recommended for general PII)* | 42 PII types, best span-level F1 on SPY (beats the others), <100ms — but its fixed taxonomy has **no hostname or organization label**, so it does not replace `ollama` for pentest-specific redaction | `pip install "anonproxy[gliner2]"`, CPU-friendly |
364
+ | `openai-privacy-filter` | ~96–97% F1 PII (Apache-2.0, OpenAI) | `pip install "anonproxy[openai-pii]"` (torch) |
365
+ | `gliner` | zero-shot person/org/username/hostname/email — the older urchade model, but because it's zero-shot you can *ask* it for `hostname`/`organization`, which `gliner2` can't be asked for | `pip install "anonproxy[gliner]"`, CPU-friendly |
366
+ | `piiranha` | high-accuracy passwords/emails/usernames (6 languages) | `pip install "anonproxy[piiranha]"` (torch) |
367
+ | `anonymizer-slm` | purpose-built PII detect+replace (Eternis Qwen3 fine-tune) | import GGUF via `models/anonymizer-slm.Modelfile` |
368
+
369
+ All four transformer backends (`gliner`, `gliner2`, `piiranha`, `openai-privacy-filter`)
370
+ chunk large inputs internally — a transformer has a fixed context window, and
371
+ handing one a whole multi-hundred-KB HTTP response either truncates coverage
372
+ silently or exhausts memory. Chunking bounds memory while still scanning the
373
+ full input.
374
+
375
+ > **Best recall for general PII:** `gliner2` (Fastino's GLiNER2-PII, May 2026)
376
+ > currently tops the SPY PII benchmark, runs in-process (no external service to
377
+ > keep alive), and surfaces failures cleanly instead of swallowing them.
378
+ > **It does not catch bare hostnames or client/org names** — that's the #1
379
+ > redaction need on a pentest engagement, and nothing in its 42-label PII
380
+ > taxonomy covers it. Don't drop `ollama` (or `gliner`) in favor of `gliner2`
381
+ > alone; stack them instead: **`ANONPROXY_DETECTORS=regex,gliner2,ollama`**.
382
+ > (When pulling `openai/privacy-filter`, use exactly that org — typosquats have
383
+ > appeared.)
384
+ >
385
+ > **Web-app testing:** the regex floor now covers payment cards, session cookies,
386
+ > Bearer/Basic auth and SSNs, so a lot of HTTP traffic is handled deterministically.
387
+ > But **names and addresses in request/response bodies are not regex-detectable** —
388
+ > for those add a PII model. Always confirm on real traffic with
389
+ > `python -m anonproxy verify`.
390
+
391
+ ```bash
392
+ # default, out of the box
393
+ ANONPROXY_DETECTORS=regex,ollama python -m anonproxy serve
394
+
395
+ # recommended: fast general-PII coverage (gliner2) + hostnames/orgs/names (ollama)
396
+ ANONPROXY_DETECTORS=regex,gliner2,ollama python -m anonproxy serve
397
+
398
+ # no external service at all — zero-shot gliner covers hostname/org itself
399
+ ANONPROXY_DETECTORS=regex,gliner,gliner2 python -m anonproxy serve
400
+
401
+ # stack several — order is just declaration order; regex always wins for
402
+ # structured types so a hash stays a hash
403
+ ANONPROXY_DETECTORS=regex,piiranha,ollama python -m anonproxy serve
404
+ ```
405
+
406
+ The regex floor always wins type classification for structured data (so a hash
407
+ is never mis-typed as an org name), contextual backends add recall, and the
408
+ consistency rescan re-catches anything seen once. `python -m anonproxy verify`
409
+ prints the active chain and confirms coverage.
410
+
411
+ ### Hostnames & the scope seed
412
+
413
+ The regex floor catches **fully-qualified** hostnames (`dc01.acme.local`,
414
+ `shop.acme.com`, `portal.acme.dev`) via a broad TLD list. It deliberately does
415
+ **not** guess **bare** hostnames (`DC01`, `WEB-PRD-03`) — there's no safe pattern
416
+ that separates them from ordinary words.
417
+
418
+ Two ways to cover bare names:
419
+
420
+ 1. **Scope seed (recommended, deterministic).** Tell Anonproxy your engagement
421
+ scope and every occurrence is anonymized, no model needed:
422
+
423
+ ```bash
424
+ # inline
425
+ python -m anonproxy serve --engagement acme-2026 \
426
+ --scope "acme.com,portal.acme.com,DC01,WEB-PRD-03,Acme Corp"
427
+ # or a file (one term per line, optional value=TYPE)
428
+ python -m anonproxy serve --scope-file acme-2026-scope.txt
429
+ ```
430
+
431
+ The wizard writes a starter `<engagement>-scope.txt` (with examples) for you
432
+ and wires it into `.env` — just keep editing it as scope grows. The `--scope` /
433
+ `--scope-file` flags work on `serve`, `verify`, and `anon` too; or set
434
+ `ANONPROXY_SCOPE` / `ANONPROXY_SCOPE_FILE`.
435
+
436
+ Scope terms run as part of the floor (always on, even regex-only), match whole
437
+ tokens only (so `acme` won't touch `acmespeak`), and get the usual consistent,
438
+ reversible surrogates.
439
+
440
+ 2. **A contextual backend** (`ollama` or `gliner` — not `gliner2`, which doesn't
441
+ request hostname/org labels) infers hostnames/org names it wasn't told about
442
+ — good for catching scope you forgot to list.
443
+
444
+ > **Very short bare names** (`db`, `sql`, `dc1`): a contextual single token
445
+ > under 4 characters is dropped as probable noise, not a hostname. Put short
446
+ > names on the scope list (deterministic, always caught), or lower
447
+ > `ANONPROXY_CONTEXTUAL_MIN_LEN`.
448
+
449
+ Use both: seed what you know, let the model catch the rest.
450
+
451
+ ## Verify coverage before an engagement
452
+
453
+ ```bash
454
+ python -m anonproxy verify # uses your local Ollama if it's running
455
+ python -m anonproxy verify --no-llm # regex floor only
456
+ python -m anonproxy verify --show-mappings # also print anonymized output + original→surrogate table
457
+ ANONPROXY_DETECTORS=regex,gliner2 python -m anonproxy verify # test a specific chain
458
+ ```
459
+
460
+ `--show-mappings` (alias `--audit`) is the audit view for a verify run — it shows
461
+ exactly what the LLM would receive and every swap it made. (The `/audit` web
462
+ dashboard is for *live proxy* traffic; verify runs in a throwaway vault so it
463
+ won't clutter a real engagement.)
464
+
465
+ Runs realistic nmap / secretsdump / netexec / config / HTTP outputs through the
466
+ full pipeline and reports any **leaks** (sensitive strings that survived) and
467
+ **round-trip** failures. It prints the active detector chain, and whichever
468
+ contextual backends are up are used automatically — so this also confirms your
469
+ local model works. Secrets only a contextual backend can catch (bare hostnames,
470
+ unlabelled creds) are shown as *needs-contextual* in regex-only mode rather than
471
+ counted as leaks. It also runs an **adversarial "repeat the context verbatim"
472
+ probe** that asserts no regex-layer secret could appear in what was sent
473
+ upstream, and a **tool-call payload probe** that checks an Anthropic `tool_use`
474
+ block / OpenAI `tool_calls[].function.arguments` echoed back in conversation
475
+ history doesn't leak a real value either — that path bypassed anonymization
476
+ entirely until this check existed. Exit code is non-zero if anything real leaks.
477
+
478
+ Check what's actually active any time:
479
+
480
+ ```bash
481
+ curl -s http://127.0.0.1:8099/anonproxy/health | python -m json.tool
482
+ # -> detectors[]: name, available, model/effective_model, detail
483
+ ```
484
+
485
+ If a configured Ollama model isn't pulled, the detector auto-falls back to an
486
+ installed one and says so in `health` and `verify` (so "Ollama is running but no
487
+ model" can't silently degrade you to regex-only). If Ollama dies or times out
488
+ mid-engagement, the next request re-checks reachability instead of trusting a
489
+ stale "available" — `health` will flip to `available: false` with the reason,
490
+ rather than staying green while quietly returning zero detections.
491
+
492
+ ## Tests
493
+
494
+ ```bash
495
+ python3 -m pytest -q # 120 tests: round-trip, streaming, proxy, audit, verify, detectors, tool-calls, vault, profiles, llm_detector, webapp, scope, config, polish
496
+ python3 scripts/benchmark_roundtrip.py # naive vs tolerant pass-rate table
497
+ ```
498
+
499
+ ## Techniques & prior art
500
+
501
+ Anonproxy implements the reversible-anonymization techniques that fit a pentest
502
+ LLM pipeline, and deliberately skips the ones that don't:
503
+
504
+ | Technique | In Anonproxy |
505
+ |---|---|
506
+ | Format-preserving masking | ✅ surrogates are valid instances of their type |
507
+ | Pseudonymization via consistent keyed-hash substitution | ✅ HMAC keyed on the engagement id + vault |
508
+ | Tolerant, streaming-safe restoration | ✅ the core improvement over match/replace |
509
+ | Generalization (ranges) / nulling (redaction) | ➖ not applicable to pentest infra data |
510
+ | Synthetic data generation | ➖ surrogates are synthetic but kept structure-faithful |
511
+
512
+ **One method to keep in mind — linkability.** Consistent surrogates (the same
513
+ input always maps to the same fake) make the AI's reasoning coherent across a
514
+ session, but that same consistency means a provider could, in principle,
515
+ *correlate* requests over time even without the real values (see RAT-Bench and
516
+ "localized adversarial anonymization", 2026). For pentest work the consistency is
517
+ usually worth it (you want the model to track "the same host" across turns). If
518
+ you need stronger anonymity against correlation, rotate the engagement id per
519
+ session — reversibility still works (it's vault-backed), only the surrogate
520
+ values change.
521
+
522
+ Inspired by [DontFeedTheAI](https://github.com/zeroc00I/DontFeedTheAI) and informed by:
523
+
524
+ - Fastino, [*GLiNER2-PII*](https://huggingface.co/fastino/gliner2-privacy-filter-PII-multi) — current SOTA PII span extraction; the recommended `gliner2` backend.
525
+ - OpenAI, [*Privacy Filter*](https://huggingface.co/openai/privacy-filter) — open-weight local PII classifier; the `openai-privacy-filter` backend.
526
+ - Eternis, [*Anonymizer SLM series*](https://huggingface.co/blog/pratyushrt/anonymizerslm) — purpose-built Qwen3 fine-tune for surgical PII detect+replace; the optional `anonymizer-slm` backend.
527
+ - Red-Gate Simple Talk, [*How to anonymize PII in LLM pipelines*](https://www.red-gate.com/simple-talk/data-security-privacy-compliance/how-to-anonymize-pii-in-llm-pipelines-5-key-techniques-explained/) — the five-technique taxonomy above and the adversarial "repeat the context" leak test now in `verify`.
528
+ - Earlier/optional models: [GLiNER](https://huggingface.co/urchade/gliner_multi_pii-v1), [Piiranha](https://huggingface.co/iiiorg/piiranha-v1-detect-personal-information).
529
+
530
+ ## Scope & limits
531
+
532
+ This is a **risk-reduction layer, not a privacy guarantee** (same honest framing
533
+ as the original). It does not defend against query-pattern correlation, prompt
534
+ injection in tool output, or compromise of the local host. It only anonymizes
535
+ text — pasted screenshots or other non-text content bypass it entirely. It
536
+ assumes a single proxy process; the vault's in-memory cache has no cross-process
537
+ invalidation, so don't run it behind multiple workers (`serve` always starts
538
+ one). It is not a substitute for reading what your NDA and engagement contract
539
+ allow before using any cloud AI on client data. Verify coverage per engagement
540
+ with the `/audit` page, `export`, and the test suite.
541
+
542
+ ## License
543
+
544
+ MIT.