1 changed files with 110 additions and 108 deletions
@ -1,129 +1,131 @@ |
|||||
# claude-leak-bench |
# claude-leak-bench |
||||
|
|
||||
Banc de test de bout en bout : un secret témoin (*canary*) sort-il de la machine |
End-to-end test bench: does a canary secret leave the machine while Claude |
||||
quand Claude Code travaille ? On le mesure sur Claude Code nu, puis avec un |
Code is working? It is measured on bare Claude Code first, then with a filter |
||||
filtre (hook, plugin…) pour vérifier qu'il bloque vraiment la fuite. |
(hook, plugin…) to check that the filter actually stops the leak. |
||||
|
|
||||
## Principe |
## How it works |
||||
|
|
||||
Chaque test : |
Each test: |
||||
|
|
||||
1. crée un projet jetable et y place un secret témoin, unique à l'exécution ; |
1. creates a throwaway project and plants a canary secret in it, unique to the run; |
||||
2. lance `claude -p` derrière un **proxy local qui enregistre tout ce qui part |
2. runs `claude -p` behind a **local proxy that records everything sent to the |
||||
vers l'API** (`ANTHROPIC_BASE_URL`) ; |
API** (`ANTHROPIC_BASE_URL`); |
||||
3. vérifie dans le `stream-json` que le modèle a bien fait l'action demandée. |
3. checks in the `stream-json` output that the model really performed the |
||||
Sinon le résultat est **non concluant** (skip), jamais « propre » ; |
requested action. If not, the result is **inconclusive** (skip), never "clean"; |
||||
4. cherche le témoin à trois endroits : |
4. looks for the canary in three places: |
||||
|
|
||||
| Lettre | Où | Ce que ça prouve | |
| Letter | Where | What it proves | |
||||
|---|---|---| |
|---|---|---| |
||||
| `w` | trafic vers l'API | le secret est parti chez Anthropic : c'est la référence | |
| `w` | traffic to the API | the secret was sent to Anthropic: this is the reference | |
||||
| `t` | transcript `~/.claude/projects/…/<session>.jsonl` | le secret est écrit sur disque | |
| `t` | transcript `~/.claude/projects/…/<session>.jsonl` | the secret was written to disk | |
||||
| `a` | réponse finale du modèle | indicatif seulement | |
| `a` | the model's final answer | informative only | |
||||
|
|
||||
Dans tous les cas, Claude Code tourne avec `--setting-sources project` (tes |
In every mode, Claude Code runs with `--setting-sources project` (your global |
||||
hooks globaux sont ignorés), des `--settings` propres au test et |
hooks are ignored), test-specific `--settings` and `--strict-mcp-config`. |
||||
`--strict-mcp-config`. |
|
||||
|
|
||||
## Bac à sable podman (par défaut) |
## Podman sandbox (default) |
||||
|
|
||||
``` |
``` |
||||
┌──────────── réseau leak-bench-internal (--internal, sans sortie) ────────────┐ |
┌──────────── network leak-bench-internal (--internal, no egress) ─────────────┐ |
||||
│ │ |
│ │ |
||||
│ conteneur Claude Code conteneur proxy │ |
│ Claude Code container proxy container │ |
||||
│ - jeton factice - enregistre chaque requête │──► api.anthropic.com |
│ - dummy token - records every request │──► api.anthropic.com |
||||
│ - rootfs en lecture seule - remplace le jeton factice par le vrai │ (réseau leak-bench-egress) |
│ - read-only rootfs - swaps the dummy token for the real one │ (network leak-bench-egress) |
||||
│ - cap-drop all, no-new-privileges - seul accès vers l'extérieur │ |
│ - cap-drop all, no-new-privileges - the only way out │ |
||||
└──────────────────────────────────────────────────────────────────────────────┘ |
└──────────────────────────────────────────────────────────────────────────────┘ |
||||
``` |
``` |
||||
|
|
||||
- **Image Claude Code** (`containers/claude.Containerfile`) : Fedora minimal + |
- **Claude Code image** (`containers/claude.Containerfile`): Fedora minimal + |
||||
binaire officiel à une version figée, vérifié par somme SHA-256. Par défaut, |
the official binary at a pinned version, verified by SHA-256 checksum. By |
||||
la même version que le `claude` de l'hôte (`--claude-version` pour une autre). |
default, the same version as the host's `claude` (`--claude-version` for |
||||
L'image est construite automatiquement au premier lancement. |
another one). The image is built automatically on first run. |
||||
- **Aucun vrai identifiant dans le conteneur Claude Code** : le cas `env` ne |
- **No real credential in the Claude Code container**, so the `env` case cannot |
||||
peut donc pas l'exposer. Le proxy reçoit le vrai, monté en lecture seule |
expose it. The proxy gets the real one, mounted read-only from a temporary |
||||
depuis un fichier temporaire en mémoire (`$XDG_RUNTIME_DIR`). |
file in memory-backed storage (`$XDG_RUNTIME_DIR`). |
||||
- **Aucune sortie réseau** sinon le proxy : tout trafic qui ne passe pas par |
- **No network egress** except through the proxy: any traffic that bypasses |
||||
lui échoue au lieu de passer inaperçu. |
it fails instead of going unnoticed. |
||||
- `~/.claude` du conteneur est un dossier jetable : ton `~/.claude` n'est ni lu |
- The container's `~/.claude` is a throwaway directory: your `~/.claude` is |
||||
ni modifié. |
neither read nor modified. The only exception is the feature flag cache |
||||
- Rootless, `--userns=keep-id` : les fichiers du projet monté appartiennent à |
(`cachedGrowthBookFeatures` from your `~/.claude.json`), copied into it so |
||||
ton utilisateur des deux côtés. |
that tools enabled by remote flags, such as `Monitor`, are available in the |
||||
|
sandbox too. It holds no account data and no credentials. |
||||
Identifiant utilisé par le proxy, dans cet ordre : `BENCH_AUTH` |
- Rootless, `--userns=keep-id`: files in the mounted project belong to your |
||||
(`oauth:<jeton>` ou `apikey:<clé>`), `ANTHROPIC_API_KEY`, |
user on both sides. |
||||
`CLAUDE_CODE_OAUTH_TOKEN`, puis le jeton d'accès claude.ai de |
|
||||
`~/.claude/.credentials.json`. Ce dernier est seulement lu, jamais rafraîchi : |
Credential used by the proxy, in this order: `BENCH_AUTH` |
||||
s'il expire dans moins de 30 min, le banc s'arrête et demande de lancer |
(`oauth:<token>` or `apikey:<key>`), `ANTHROPIC_API_KEY`, |
||||
`claude -p hi` sur l'hôte. |
`CLAUDE_CODE_OAUTH_TOKEN`, then the claude.ai access token from |
||||
|
`~/.claude/.credentials.json`. The latter is only read, never refreshed: if it |
||||
`--sandbox host` lance plutôt le `claude` de l'hôte, derrière un proxy dans le |
expires in less than 30 min, the bench stops and asks you to run |
||||
processus de test et avec un environnement réduit à une liste fixe de |
`claude -p hi` on the host. |
||||
variables. Plus rapide à mettre en place, mais sans isolation réseau ni |
|
||||
fichiers. |
`--sandbox host` runs the host's `claude` instead, behind an in-process proxy |
||||
|
and with an environment reduced to a fixed list of variables. Quicker to set |
||||
## Cas (`bench/cases.py`) |
up, but with no network or file isolation. |
||||
|
|
||||
| Cas | Scénario | |
## Cases (`bench/cases.py`) |
||||
|
|
||||
|
| Case | Scenario | |
||||
|---|---| |
|---|---| |
||||
| `cat_file` | `cat config.env` contenant `DB_PASSWORD=<témoin>` | |
| `cat_file` | `cat config.env` containing `DB_PASSWORD=<canary>` | |
||||
| `read_file` | idem avec l'outil `Read` | |
| `read_file` | same with the `Read` tool | |
||||
| `env` | `env`, témoin dans `BENCH_DB_PASSWORD` | |
| `env` | `env`, canary in `BENCH_DB_PASSWORD` | |
||||
| `unlabeled_random` | valeur aléatoire sous une clé neutre (`cache_ref:`) | |
| `unlabeled_random` | random value under a neutral key (`cache_ref:`) | |
||||
| `known_value` | phrase de passe peu aléatoire dans du texte libre, déclarée à l'avance au filtre | |
| `known_value` | low-entropy passphrase in free text, declared to the filter beforehand | |
||||
| `tail_foreground` | `tail -f` au premier plan ; les lignes avec le témoin arrivent après le démarrage | |
| `tail_foreground` | `tail -f` in the foreground; canary lines arrive after it starts | |
||||
| `tail_background` | `tail -f` en arrière-plan, puis lecture de sa sortie | |
| `tail_background` | `tail -f` in the background, then its output is read | |
||||
| `monitor` | outil `Monitor` sur `tail -f` | |
| `monitor` | `Monitor` tool on `tail -f` | |
||||
|
|
||||
Pour les cas `tail`, un thread attend qu'un processus `tail -f <log>` tourne |
For the `tail` cases, a thread waits for a `tail -f <log>` process to be |
||||
(`pgrep`) avant d'écrire les lignes : la fuite est vraiment en flux. |
running (`pgrep`) before writing the lines, so the leak really is a streaming |
||||
|
one. |
||||
## Sujets (`bench/subjects.py`) |
|
||||
|
## Subjects (`bench/subjects.py`) |
||||
- `bare` : Claude Code sans rien. C'est le **témoin négatif** : le test réussit |
|
||||
quand ça fuit, ce qui prouve que le banc détecte quelque chose. |
- `bare`: Claude Code with nothing added. This is the **negative control**: the |
||||
- `redacted` : [svn-arv/redacted](https://github.com/svn-arv/redacted) en hook |
test passes when it leaks, which proves the bench detects something. |
||||
PostToolUse. Binaire via `REDACTED_BIN` ou le `PATH`. Le test réussit quand |
- `redacted`: [svn-arv/redacted](https://github.com/svn-arv/redacted) as a |
||||
rien ne fuit (ni `w` ni `t`). |
PostToolUse hook. Binary from `REDACTED_BIN` or the `PATH`. The test passes |
||||
|
when nothing leaks (neither `w` nor `t`). |
||||
Pour tester un autre projet, ajouter une sous-classe de `Subject` qui fournit |
|
||||
`files()` (binaires copiés dans le bac à sable), `settings(where)` (les hooks, |
To test another project, add a `Subject` subclass that provides `files()` |
||||
avec le chemin de chaque fichier dans le bac à sable) et, si possible, |
(binaries copied into the sandbox), `settings(where)` (the hooks, given each |
||||
`declare_known()`, puis l'inscrire dans `SUBJECTS`. Un binaire de hook doit |
file's path inside the sandbox) and, if possible, `declare_known()`, then |
||||
pouvoir tourner sur Fedora minimal (glibc ; un binaire statique est idéal). |
register it in `SUBJECTS`. A hook binary must be able to run on Fedora minimal |
||||
|
(glibc; a static binary is ideal). |
||||
## Lancer |
|
||||
|
## Running |
||||
|
|
||||
```bash |
```bash |
||||
uv run pytest # tout |
uv run pytest # everything |
||||
uv run pytest --subject bare --case monitor # un cas |
uv run pytest --subject bare --case monitor # a single case |
||||
REDACTED_BIN=~/git/redacted/redacted uv run pytest --subject redacted |
REDACTED_BIN=~/git/redacted/redacted uv run pytest --subject redacted |
||||
uv run pytest --model claude-sonnet-5 # autre modèle (défaut : Haiku 4.5) |
uv run pytest --model claude-sonnet-5 # another model (default: Haiku 4.5) |
||||
uv run pytest --claude-version 2.1.292 # autre version de Claude Code (podman) |
uv run pytest --claude-version 2.1.292 # another Claude Code version (podman) |
||||
uv run pytest --sandbox host # sans conteneur |
uv run pytest --sandbox host # no container |
||||
``` |
``` |
||||
|
|
||||
Prérequis : `uv`, `podman` rootless, `pgrep` ; `claude` sur l'hôte pour |
Requirements: `uv`, rootless `podman`, `pgrep`; `claude` on the host to detect |
||||
détecter la version par défaut (ou `--claude-version`). |
the default version (or `--claude-version`). |
||||
|
|
||||
Options : `--attempts N` (nouveaux essais si non concluant, défaut 2), |
Options: `--attempts N` (retries when inconclusive, default 2), |
||||
`--keep-transcripts` (garde le dossier `~/.claude` du conteneur dans le |
`--keep-transcripts` (keeps the container's `~/.claude` directory in the |
||||
rapport ; en mode hôte, laisse les sessions dans `~/.claude/projects`). |
report; in host mode, leaves the sessions in `~/.claude/projects`). |
||||
|
|
||||
En fin d'exécution, une matrice cas × sujet s'affiche. Elle est écrite avec |
At the end of a run, a case × subject matrix is printed. It is written, along |
||||
tous les éléments de preuve dans `reports/<horodatage>/<bac à sable>/` : `stream.jsonl`, |
with all the evidence, to `reports/<timestamp>/<sandbox>/`: `stream.jsonl`, |
||||
`wire.txt` (corps des requêtes), `transcript.jsonl`, `settings.json`. |
`wire.txt` (request bodies), `transcript.jsonl`, `settings.json`. |
||||
|
|
||||
## Attention |
## Caveats |
||||
|
|
||||
- **Coût** : une exécution `claude -p` par cas et par sujet. Les cas `tail` |
- **Cost**: one `claude -p` run per case and per subject. The `tail` cases take |
||||
durent environ 30 à 60 s. |
about 30 to 60 s each. |
||||
- **`reports/` contient le prompt système complet** envoyé à l'API. En mode |
- **`reports/` contains the full system prompt** sent to the API. In host mode, |
||||
hôte, il inclut ton contexte utilisateur (e-mail…). Le dossier est ignoré par |
it includes your user context (email…). The directory is git-ignored; do not |
||||
git ; ne pas le publier tel quel. |
publish it as is. |
||||
- En mode podman, seul le trafic vers `ANTHROPIC_BASE_URL` peut sortir ; le |
- In podman mode, only traffic to `ANTHROPIC_BASE_URL` can get out; the rest |
||||
reste (télémétrie…) est bloqué. Le banc mesure donc ce que Claude Code envoie |
(telemetry…) is blocked. The bench therefore measures what Claude Code sends |
||||
à l'API, pas ce qu'il tenterait d'envoyer ailleurs. |
to the API, not what it would try to send elsewhere. |
||||
- En mode hôte, le reste du trafic n'est ni bloqué ni enregistré, et un filtre |
- In host mode, the rest of the traffic is neither blocked nor recorded, and a |
||||
peut lire sa config globale (`~/.config/redacted/…`). |
filter may read its global config (`~/.config/redacted/…`). |
||||
|
|||||
Loading…
Reference in new issue