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.
- anonproxy-0.1.1/LICENSE +21 -0
- anonproxy-0.1.1/PKG-INFO +544 -0
- anonproxy-0.1.1/README.md +514 -0
- anonproxy-0.1.1/anonproxy/__init__.py +22 -0
- anonproxy-0.1.1/anonproxy/__main__.py +4 -0
- anonproxy-0.1.1/anonproxy/audit.py +195 -0
- anonproxy-0.1.1/anonproxy/cli.py +365 -0
- anonproxy-0.1.1/anonproxy/config.py +156 -0
- anonproxy-0.1.1/anonproxy/detectors/__init__.py +145 -0
- anonproxy-0.1.1/anonproxy/detectors/anonymizer_slm.py +119 -0
- anonproxy-0.1.1/anonproxy/detectors/gliner2_detector.py +89 -0
- anonproxy-0.1.1/anonproxy/detectors/gliner_detector.py +76 -0
- anonproxy-0.1.1/anonproxy/detectors/llm_detector.py +219 -0
- anonproxy-0.1.1/anonproxy/detectors/openai_pii_detector.py +82 -0
- anonproxy-0.1.1/anonproxy/detectors/piiranha_detector.py +83 -0
- anonproxy-0.1.1/anonproxy/detectors/regex_detector.py +190 -0
- anonproxy-0.1.1/anonproxy/detectors/scope_detector.py +98 -0
- anonproxy-0.1.1/anonproxy/engine.py +253 -0
- anonproxy-0.1.1/anonproxy/profiles.py +228 -0
- anonproxy-0.1.1/anonproxy/proxy/__init__.py +1 -0
- anonproxy-0.1.1/anonproxy/proxy/app.py +296 -0
- anonproxy-0.1.1/anonproxy/proxy/streaming.py +137 -0
- anonproxy-0.1.1/anonproxy/proxy/transform.py +115 -0
- anonproxy-0.1.1/anonproxy/restorer.py +197 -0
- anonproxy-0.1.1/anonproxy/surrogates.py +279 -0
- anonproxy-0.1.1/anonproxy/vault.py +165 -0
- anonproxy-0.1.1/anonproxy/verify.py +273 -0
- anonproxy-0.1.1/anonproxy/wizard.py +226 -0
- anonproxy-0.1.1/anonproxy.egg-info/PKG-INFO +544 -0
- anonproxy-0.1.1/anonproxy.egg-info/SOURCES.txt +48 -0
- anonproxy-0.1.1/anonproxy.egg-info/dependency_links.txt +1 -0
- anonproxy-0.1.1/anonproxy.egg-info/entry_points.txt +2 -0
- anonproxy-0.1.1/anonproxy.egg-info/requires.txt +26 -0
- anonproxy-0.1.1/anonproxy.egg-info/top_level.txt +1 -0
- anonproxy-0.1.1/pyproject.toml +32 -0
- anonproxy-0.1.1/setup.cfg +4 -0
- anonproxy-0.1.1/tests/test_audit.py +91 -0
- anonproxy-0.1.1/tests/test_config.py +33 -0
- anonproxy-0.1.1/tests/test_detectors.py +119 -0
- anonproxy-0.1.1/tests/test_llm_detector.py +55 -0
- anonproxy-0.1.1/tests/test_polish.py +174 -0
- anonproxy-0.1.1/tests/test_profiles.py +122 -0
- anonproxy-0.1.1/tests/test_proxy.py +268 -0
- anonproxy-0.1.1/tests/test_roundtrip.py +127 -0
- anonproxy-0.1.1/tests/test_scope.py +53 -0
- anonproxy-0.1.1/tests/test_streaming.py +50 -0
- anonproxy-0.1.1/tests/test_tool_calls.py +204 -0
- anonproxy-0.1.1/tests/test_vault.py +28 -0
- anonproxy-0.1.1/tests/test_verify.py +73 -0
- anonproxy-0.1.1/tests/test_webapp.py +55 -0
anonproxy-0.1.1/LICENSE
ADDED
|
@@ -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.
|
anonproxy-0.1.1/PKG-INFO
ADDED
|
@@ -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.
|