From ef508b43a4dd8516245884462b15f6a96cf4c95e Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Sun, 11 Oct 2026 13:43:57 +0200 Subject: [PATCH] Add rdx-proxy subject: a redacting API proxy as an interceptor Subjects can now provide a sidecar, built as an image from their checkout, used for setup steps in the project (prepare) and/or as a proxy between Claude Code and the recording proxy (interceptor). rdx-proxy benches paroque28/claude-code-redact in proxy mode. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GZY2T82aEHnuVC4ALzRcPh --- README.md | 69 ++++++++++++++++++++------ bench/runner.py | 94 ++++++++++++++++++++++++++++++------ bench/subjects.py | 91 ++++++++++++++++++++++++++++++++++ containers/rdx.Containerfile | 9 ++++ tests/test_leaks.py | 3 +- 5 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 containers/rdx.Containerfile diff --git a/README.md b/README.md index b28444f..eb00a13 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,20 @@ Run on 2026-10-11, podman sandbox, Claude Code 2.1.283, model Haiku 4.5, `redacted` at commit `0b4dd23` (`main`, Go version), `redacted-pr47` at commit `b88d507` ([svn-arv/redacted#47](https://github.com/svn-arv/redacted/pull/47), Rust rewrite 1.0.0), as is and with the entropy heuristic on -(`redacted-pr47-heuristic`). - -| Case | bare | redacted | redacted-pr47 | redacted-pr47-heuristic | -|---|---|---|---|---| -| `cat_file` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `read_file` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `env` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `unlabeled_random` | πŸ”΄ | πŸ”΄ | πŸ”΄ | 🟒 | -| `known_value` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `tail_foreground` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `tail_background` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | -| `monitor` | πŸ”΄ | πŸ”΄ | πŸ”΄ | πŸ”΄ | +(`redacted-pr47-heuristic`). `rdx-proxy`: +[paroque28/claude-code-redact](https://github.com/paroque28/claude-code-redact) +at `d9c57fa` plus two local fixes (see below), 3 runs, one dot per run. + +| Case | bare | redacted | redacted-pr47 | redacted-pr47-heuristic | rdx-proxy | +|---|---|---|---|---|---| +| `cat_file` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | πŸŸ πŸ”΄πŸŸ  | +| `read_file` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | πŸ”΄πŸŸ πŸ”΄ | +| `env` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | πŸŸ πŸ”΄πŸŸ  | +| `unlabeled_random` | πŸ”΄ | πŸ”΄ | πŸ”΄ | 🟒 | πŸ”΄πŸ”΄πŸ”΄ | +| `known_value` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | 🟠🟠🟠 | +| `tail_foreground` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | πŸŸ πŸ”΄πŸŸ  | +| `tail_background` | πŸ”΄ | πŸ”΄ | 🟒 | 🟒 | 🟠🟠🟠 | +| `monitor` | πŸ”΄ | πŸ”΄ | πŸ”΄ | πŸ”΄ | πŸŸ πŸ”΄πŸ”΄ | πŸ”΄ sent to the Anthropic API Β· 🟠 not sent to the API, but written to the transcript on disk Β· 🟒 neither @@ -70,8 +72,24 @@ transcript on disk Β· 🟒 neither heuristic (off by default). - **redacted-pr47-heuristic**: with the heuristic on, `unlabeled_random` is caught too (`cache_ref: [REDACTED ...B95I]`, pattern `secret_value`). +- **rdx-proxy**: as an API proxy, it sees everything sent to the API, + `Monitor` notifications included, but the transcript always keeps the + secret, so 🟠 is its best result. Apart from the declared `known_value`, + only its entropy detector catches the canaries: Shannon entropy of at least + 4.5 bits per character, on a value after `=` or in quotes. A random + 32-character value stays under that threshold about one time in three + (68/200 in a direct test of its redactor), and every πŸ”΄ above is a canary + under 4.5 bits, every 🟠 one at 4.5 or more. `unlabeled_random` + (`cache_ref: `) matches no detector. Its context detector only knows + quoted values and `export` lines. + At `d9c57fa`, the proxy answers 500 to every request of a project with + `.redaction_rules` (a `NameError` on `_cache` since `b6c4606`), so Claude + Code cannot work at all, and its streaming responses are cut off because the + HTTP client is closed before the stream is read. The bench ran a local + branch with both fixed (`_cache` β†’ `cache`, a client owned by the stream). - **`monitor`**: `Monitor` events reach the model as notifications that never go through PostToolUse hooks, so a PostToolUse filter cannot see them at all. + Only an API proxy such as `rdx-proxy` can catch them. - **`tail_foreground`**: when the Bash timeout expires, Claude Code moves the command to the background and the model reads its output file with `Read`, so this case ends up like `tail_background`. @@ -98,6 +116,11 @@ re-run the bench rather than relying on this table. - **No real credential in the Claude Code container**, so the `env` case cannot expose it. The proxy gets the real one, mounted read-only from a temporary file in memory-backed storage (`$XDG_RUNTIME_DIR`). +- **Interceptor**: a subject that is a redacting API proxy (`rdx-proxy`) runs + in a third container on the internal network, between Claude Code and the + recording proxy, with the same hardening. Its image is built from the + subject's checkout and tagged with its commit. The project is mounted + read-only at the same path (`/work`) as in the Claude Code container. - **No network egress** except through the proxy: any traffic that bypasses it fails instead of going unnoticed. - The container's `~/.claude` is a throwaway directory: your `~/.claude` is @@ -152,11 +175,23 @@ one. The heuristic is read from the global config only, so the hook runs with a `HOME` of its own holding `.config/redacted/config.yaml`. +- `rdx-proxy`: [paroque28/claude-code-redact](https://github.com/paroque28/claude-code-redact) + in proxy mode, its recommended mode. Checkout from `RDX_SRC`. It redacts + requests on their way to the API and puts the real values back in the + responses, so the transcript on disk always holds the secret: its best + possible result is 🟠. rdx redacts nothing for a project without + `.redaction_rules`, so the subject writes an empty one (built-in detectors + only), then performs the file steps of `rdx setup --proxy` with rdx's own + code (deny permissions in `.claude/settings.json`, `RDX.md`, `CLAUDE.md`). + To test another project, add a `Subject` subclass that provides `files()` (binaries copied into the sandbox), `settings(where)` (the hooks, given each file's path inside the sandbox) and, if possible, `declare_known()`, then register it in `SUBJECTS`. A hook binary must be able to run on Fedora minimal -(glibc; a static binary is ideal). +(glibc; a static binary is ideal). A subject that is not a hook provides a +`Sidecar` (its checkout and a Containerfile) and uses it in `prepare()` (setup +steps run in the project) and/or `interceptor()` (a proxy inserted before the +recording proxy). ## Running @@ -165,6 +200,7 @@ uv run pytest # everything uv run pytest --subject bare --case monitor # a single case REDACTED_BIN=~/git/redacted/redacted uv run pytest --subject redacted REDACTED_PR47_BIN=~/git/redacted-pr47/target/release/redacted uv run pytest --subject redacted-pr47 --subject redacted-pr47-heuristic +RDX_SRC=~/git/claude-code-redact uv run pytest --subject rdx-proxy uv run pytest --model claude-sonnet-5 # another model (default: Haiku 4.5) uv run pytest --claude-version 2.1.292 # another Claude Code version (podman) uv run pytest --sandbox host # no container @@ -179,7 +215,8 @@ report; in host mode, leaves the sessions in `~/.claude/projects`). At the end of a run, a case Γ— subject matrix is printed. It is written, along with all the evidence, to `reports///`: `stream.jsonl`, -`wire.txt` (request bodies), `transcript.jsonl`, `settings.json`. +`wire.txt` (request bodies), `transcript.jsonl`, `settings.json` and, for an +interceptor, `interceptor.log`. ## Caveats @@ -192,4 +229,6 @@ with all the evidence, to `reports///`: `stream.jsonl`, (telemetry…) is blocked. The bench therefore measures what Claude Code sends to the API, not what it would try to send elsewhere. - In host mode, the rest of the traffic is neither blocked nor recorded, and a - filter may read its global config (`~/.config/redacted/…`). + filter may read its global config (`~/.config/redacted/…`). The `rdx-proxy` + interceptor runs from its checkout with `uv run`, which creates a `.venv` + there. diff --git a/bench/runner.py b/bench/runner.py index 44f8f52..dfac911 100644 --- a/bench/runner.py +++ b/bench/runner.py @@ -5,6 +5,9 @@ Two sandboxes: network with no egress and a dummy credential. A proxy container is the only way out; it records every request and swaps in the real credential. - HostSandbox: Claude Code on the host, behind an in-process recording proxy. + +A subject's interceptor (a redacting proxy) sits between Claude Code and the +recording proxy, so what is recorded is what the interceptor let through. """ from __future__ import annotations @@ -12,6 +15,7 @@ from __future__ import annotations import json import os import shutil +import socket import subprocess import tempfile import time @@ -20,6 +24,7 @@ from dataclasses import dataclass, field from pathlib import Path from .proxy import RecordingProxy +from .subjects import Interceptor, Sidecar CLAUDE_BIN = os.environ.get("BENCH_CLAUDE_BIN", "claude") ROOT = Path(__file__).resolve().parent.parent @@ -104,18 +109,29 @@ class HostSandbox: config_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home() / ".claude")) self.projects = config_dir / "projects" + def exec(self, sidecar: Sidecar, argv: list[str], project: Path) -> None: + subprocess.run([*sidecar.host_cmd, *argv], cwd=project, check=True, capture_output=True, + env=clean_env(), timeout=300) + def run(self, prompt, *, session_id, model, project, settings, subject_files, env, artifacts, timeout, - keep_transcripts) -> RawRun: + keep_transcripts, interceptor: Interceptor | None = None) -> RawRun: settings_path = artifacts / "settings.json" settings_path.write_text(json.dumps(settings, indent=2)) proxy = RecordingProxy().start() + sidecar = None try: + base_url = proxy.base_url + if interceptor: + sidecar, base_url = self._start_interceptor(interceptor, proxy.base_url, project, artifacts) proc = subprocess.run( [CLAUDE_BIN, *claude_args(prompt, session_id, model, str(settings_path))], cwd=project, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL, - env=clean_env({"ANTHROPIC_BASE_URL": proxy.base_url, **env}), + env=clean_env({"ANTHROPIC_BASE_URL": base_url, **env}), ) finally: + if sidecar: + sidecar.terminate() + sidecar.wait(timeout=10) proxy.stop() paths = list(self.projects.glob(f"*/{session_id}*")) transcript = "\n".join(_read_tree(p) if p.is_dir() else p.read_text(errors="replace") for p in paths) @@ -124,6 +140,28 @@ class HostSandbox: shutil.rmtree(p) if p.is_dir() else p.unlink() return RawRun(proc.returncode, proc.stdout, proc.stderr, proxy.wire_text(), transcript) + @staticmethod + def _start_interceptor(icp: Interceptor, upstream: str, project: Path, + artifacts: Path) -> tuple[subprocess.Popen, str]: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + port = s.getsockname()[1] + argv = [a.format(host="127.0.0.1", port=port) for a in icp.argv] + proc = subprocess.Popen([*icp.sidecar.host_cmd, *argv], cwd=project, stdin=subprocess.DEVNULL, + stdout=(artifacts / "interceptor.log").open("w"), stderr=subprocess.STDOUT, + env=clean_env({icp.upstream_env: upstream})) + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + if proc.poll() is not None: + raise RuntimeError(f"interceptor exited: {(artifacts / 'interceptor.log').read_text()[-2000:]}") + try: + socket.create_connection(("127.0.0.1", port), timeout=1).close() + return proc, f"http://127.0.0.1:{port}" + except OSError: + time.sleep(0.2) + proc.kill() + raise RuntimeError("interceptor did not start listening") + def close(self) -> None: pass @@ -202,18 +240,31 @@ class PodmanSandbox: self.auth_file.touch(mode=0o600) self.auth_file.write_text(auth) - def _build(self, tag: str, containerfile: str, **build_args: str) -> None: + def _build(self, tag: str, containerfile: str, context: Path = ROOT, **build_args: str) -> None: if _podman("image", "exists", tag, check=False).returncode == 0: return args = [f"--build-arg={k}={v}" for k, v in build_args.items()] - _podman("build", "-q", "-f", str(ROOT / "containers" / containerfile), *args, "-t", tag, str(ROOT), + _podman("build", "-q", "-f", str(ROOT / "containers" / containerfile), *args, "-t", tag, str(context), timeout=1200) + def _sidecar_image(self, sidecar: Sidecar) -> str: + image = f"leak-bench-{sidecar.name}:{sidecar.tag}" + self._build(image, sidecar.containerfile, context=sidecar.checkout) + return image + + def _sidecar_args(self) -> list[str]: + return [f"--userns=keep-id:uid={self.UID},gid={self.UID}", "--network", self.INTERNAL, + "--cap-drop=all", "--security-opt=no-new-privileges", "--read-only", "--tmpfs", "/tmp:rw,U"] + + def exec(self, sidecar: Sidecar, argv: list[str], project: Path) -> None: + _podman("run", "--rm", *self._sidecar_args(), "-v", f"{project}:/work:z", "-w", "/work", + self._sidecar_image(sidecar), *argv, timeout=300) + def close(self) -> None: shutil.rmtree(self._secret_dir, ignore_errors=True) def run(self, prompt, *, session_id, model, project, settings, subject_files, env, artifacts, timeout, - keep_transcripts) -> RawRun: + keep_transcripts, interceptor: Interceptor | None = None) -> RawRun: work = Path(tempfile.mkdtemp(prefix="lb-", dir=artifacts.parent)) config, records, staged = work / "claude-config", work / "records", work / "subject" for d in (config, records, staged): @@ -226,24 +277,34 @@ class PodmanSandbox: shutil.copy2(staged / "settings.json", artifacts / "settings.json") tag = session_id[:8] - proxy_name, claude_name = f"lb-proxy-{tag}", f"lb-claude-{tag}" + proxy_name, claude_name, icp_name = f"lb-proxy-{tag}", f"lb-claude-{tag}", f"lb-icp-{tag}" userns = f"--userns=keep-id:uid={self.UID},gid={self.UID}" _podman("run", "-d", "--rm", "--init", "--name", proxy_name, userns, "--network", f"{self.INTERNAL},{self.EGRESS}", "--cap-drop=all", "--security-opt=no-new-privileges", "--read-only", "-v", f"{self.auth_file}:/run/secrets/bench-auth:ro,z", "-v", f"{records}:/records:Z", self.PROXY_IMAGE) + base_url = f"http://{proxy_name}:8080" try: - self._wait_ready(proxy_name) + self._wait_ready(proxy_name, "listening") + if interceptor: + argv = [a.format(host="0.0.0.0", port=8080) for a in interceptor.argv] + # Same project path as Claude Code: it may locate the project from the system prompt. + _podman("run", "-d", "--rm", "--init", "--name", icp_name, *self._sidecar_args(), + "-v", f"{project}:/work:ro,z", "-w", "/work", + "-e", f"{interceptor.upstream_env}={base_url}", + self._sidecar_image(interceptor.sidecar), *argv) + self._wait_ready(icp_name, interceptor.ready) + base_url = f"http://{icp_name}:8080" cmd = ["podman", "run", "--rm", "-i", "--name", claude_name, userns, "--network", self.INTERNAL, "--cap-drop=all", "--security-opt=no-new-privileges", "--read-only", "--pids-limit=1024", "--memory=4g", "--tmpfs", "/tmp:rw,U", "--tmpfs", "/home/bench:rw,U", "-v", f"{config}:/home/bench/.claude:Z", - "-v", f"{project}:/work:Z", + "-v", f"{project}:/work:z", "-v", f"{staged}:{self.subject_dir}:ro,Z", - "-e", f"ANTHROPIC_BASE_URL=http://{proxy_name}:8080"] + "-e", f"ANTHROPIC_BASE_URL={base_url}"] for k, v in {**self.dummy_env, **env}.items(): cmd += ["-e", f"{k}={v}"] cmd += [self.claude_image, *claude_args(prompt, session_id, model, f"{self.subject_dir}/settings.json")] @@ -254,6 +315,10 @@ class PodmanSandbox: _podman("kill", claude_name, check=False) raise finally: + if interceptor: + logs = _podman("logs", icp_name, check=False) + (artifacts / "interceptor.log").write_text(logs.stdout + logs.stderr) + _podman("kill", icp_name, check=False) _podman("kill", proxy_name, check=False) wire_file = records / "wire.jsonl" @@ -266,23 +331,24 @@ class PodmanSandbox: return RawRun(proc.returncode, proc.stdout, proc.stderr, wire, transcript) @staticmethod - def _wait_ready(name: str, timeout: float = 20) -> None: + def _wait_ready(name: str, line: str, timeout: float = 20) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: - if "listening" in _podman("logs", name, check=False).stdout: + logs = _podman("logs", name, check=False) + if line in logs.stdout + logs.stderr: return time.sleep(0.2) - raise RuntimeError(f"proxy container {name} did not start: {_podman('logs', name, check=False).stderr}") + raise RuntimeError(f"container {name} did not start: {_podman('logs', name, check=False).stderr}") def run_claude(prompt: str, *, sandbox, project: Path, settings: dict, subject_files: dict[str, Path], artifacts: Path, model: str, env: dict[str, str] | None = None, timeout: int = 600, - keep_transcripts: bool = False) -> Run: + keep_transcripts: bool = False, interceptor: Interceptor | None = None) -> Run: artifacts.mkdir(parents=True, exist_ok=True) session_id = str(uuid.uuid4()) raw = sandbox.run(prompt, session_id=session_id, model=model, project=project, settings=settings, subject_files=subject_files, env=env or {}, artifacts=artifacts, timeout=timeout, - keep_transcripts=keep_transcripts) + keep_transcripts=keep_transcripts, interceptor=interceptor) events = [] for line in raw.stdout.splitlines(): diff --git a/bench/subjects.py b/bench/subjects.py index c97688c..7b6de34 100644 --- a/bench/subjects.py +++ b/bench/subjects.py @@ -9,6 +9,11 @@ Files are copied into the sandbox: `settings(where)` receives, for each `files()` name, the path the file is visible at (a container path under podman). A name may contain `/` to lay out a directory tree. Hook binaries must run in the sandbox image (Fedora minimal, glibc). + +A subject that is not a hook (e.g. a redacting API proxy) runs as a sidecar: +an image built from its checkout under podman, a command from that checkout on +the host. `prepare()` may run it once in the project (setup steps), and +`interceptor()` inserts it between Claude Code and the recording proxy. """ from __future__ import annotations @@ -16,9 +21,41 @@ from __future__ import annotations import os import re import shutil +import subprocess import tempfile from dataclasses import dataclass from pathlib import Path +from typing import Callable + +# Runs a sidecar command with the project as working directory. +RunSidecar = Callable[["Sidecar", list[str]], None] + + +@dataclass +class Sidecar: + """A program the subject runs next to Claude Code, from a local checkout.""" + name: str # podman image: leak-bench-: + checkout: Path # build context under podman, project for `host_cmd` on the host + containerfile: str # in containers/ + host_cmd: list[str] # prefix that runs a command from the checkout on the host + + @property + def tag(self) -> str: + """The checkout's commit, or a stash commit when it has local changes.""" + git = ["git", "-C", str(self.checkout)] + rev = subprocess.run([*git, "stash", "create"], capture_output=True, text=True).stdout.strip() + rev = rev or subprocess.run([*git, "rev-parse", "HEAD"], capture_output=True, text=True, + check=True).stdout.strip() + return rev[:12] + + +@dataclass +class Interceptor: + """A sidecar proxy between Claude Code and the recording proxy.""" + sidecar: Sidecar + argv: list[str] # `{host}` and `{port}` are filled in by the sandbox + upstream_env: str # env var that receives the recording proxy's base URL + ready: str # log line printed once it listens @dataclass @@ -42,6 +79,12 @@ class Subject: """Tell the subject about values to always redact. False if unsupported.""" return False + def prepare(self, project: Path, run: RunSidecar) -> None: + """Set the project up, after the case and `declare_known` wrote theirs.""" + + def interceptor(self) -> Interceptor | None: + return None + class Bare(Subject): def __init__(self) -> None: @@ -114,9 +157,57 @@ class RedactedHeuristic(Redacted): return {"hooks": {"PostToolUse": [{"hooks": [{"type": "command", "command": cmd}]}]}} +class RdxProxy(Subject): + """github.com/paroque28/claude-code-redact (`rdx`) in proxy mode. + + An API proxy: it redacts requests on their way to the API and restores the + real values in responses, so the transcript on disk keeps the secret. It + only redacts for projects with a .redaction_rules file, found through the + "Primary working directory" of the system prompt. `prepare` performs the + file steps of `rdx setup --proxy` (deny permissions in + .claude/settings.json, RDX.md, CLAUDE.md) with rdx's own code. Checkout from + RDX_SRC. + """ + + SETUP = ("from pathlib import Path; from rdx.setup import setup as s; p = Path.cwd(); " + "f = p / s.SETTINGS_FILE; c = s._read_settings(f); s._remove_rdx_hooks(c); " + "s._merge_deny_permissions(c); s._write_settings(f, c); " + "s.write_rdx_md(p, mode='proxy'); s._ensure_claude_md_includes_rdx(p)") + + def __init__(self) -> None: + super().__init__("rdx-proxy") + src = os.environ.get("RDX_SRC") + self.sidecar = Sidecar("rdx", Path(src).expanduser() if src else Path(), "rdx.Containerfile", + ["uv", "run", "--quiet", "--project", src or ""]) + + def available(self) -> str | None: + if not os.environ.get("RDX_SRC") or not (self.sidecar.checkout / "src/rdx").is_dir(): + return "claude-code-redact checkout not found (set RDX_SRC)" + return None + + def declare_known(self, project: Path, values: list[str]) -> bool: + assert not any("'" in v for v in values), "value cannot be quoted literally" + rules = "".join(f" - id: bench-known-{i}\n pattern: '{v}'\n is_regex: false\n category: KEY\n" + for i, v in enumerate(values)) + (project / ".redaction_rules").write_text("rules:\n" + rules) + return True + + def prepare(self, project: Path, run: RunSidecar) -> None: + rules = project / ".redaction_rules" + if not rules.exists(): + rules.write_text("rules: []\n") # without it, rdx passes everything through + run(self.sidecar, ["python", "-c", self.SETUP]) + + def interceptor(self) -> Interceptor: + return Interceptor(self.sidecar, + ["python", "-m", "uvicorn", "rdx.proxy.server:app", "--host", "{host}", "--port", "{port}"], + upstream_env="RDX_UPSTREAM_URL", ready="Uvicorn running") + + SUBJECTS: dict[str, Subject] = {s.name: s for s in [ Bare(), Redacted(), Redacted("redacted-pr47", env_var="REDACTED_PR47_BIN", which=False), RedactedHeuristic(), + RdxProxy(), ]} diff --git a/containers/rdx.Containerfile b/containers/rdx.Containerfile new file mode 100644 index 0000000..11e026e --- /dev/null +++ b/containers/rdx.Containerfile @@ -0,0 +1,9 @@ +# claude-code-redact (rdx), built from a local checkout used as the build context. +# podman build -f containers/rdx.Containerfile -t leak-bench-rdx: ~/git/claude-code-redact +ARG FEDORA=44 +FROM registry.fedoraproject.org/fedora-minimal:${FEDORA} +RUN microdnf -y install --setopt=install_weak_deps=0 python3 && microdnf clean all +COPY . /src +RUN python3 -m venv /app && /app/bin/pip install --no-cache-dir /src && rm -rf /src +ENV PATH=/app/bin:$PATH HOME=/tmp +USER 1000:1000 diff --git a/tests/test_leaks.py b/tests/test_leaks.py index 4893758..c89b92a 100644 --- a/tests/test_leaks.py +++ b/tests/test_leaks.py @@ -36,6 +36,7 @@ def test_leak(subject, case_name, tmp_path, request, report_dir, record, sandbox if scenario.known and not subject.declare_known(project, [scenario.secret]): record(**row, cell="n/a") pytest.skip(f"{subject.name} cannot declare known values") + subject.prepare(project, lambda sidecar, argv: sandbox.exec(sidecar, argv, project)) if scenario.writer: scenario.writer.start() @@ -44,7 +45,7 @@ def test_leak(subject, case_name, tmp_path, request, report_dir, record, sandbox scenario.prompt, sandbox=sandbox, project=project, settings=subject.settings(where), subject_files=files, model=opt("model"), env=scenario.env, artifacts=report_dir / sandbox.name / subject.name / case_name / f"attempt{attempt}", - keep_transcripts=opt("keep_transcripts"), + keep_transcripts=opt("keep_transcripts"), interceptor=subject.interceptor(), ) finally: if scenario.writer: