agentproof-scan 0.1.2__tar.gz → 0.1.4__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.
- {agentproof_scan-0.1.2/agentproof_scan.egg-info → agentproof_scan-0.1.4}/PKG-INFO +183 -20
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/README.md +182 -19
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/__init__.py +1 -1
- agentproof_scan-0.1.4/agentproof_scan/__main__.py +10 -0
- agentproof_scan-0.1.4/agentproof_scan/adapters/__init__.py +84 -0
- agentproof_scan-0.1.4/agentproof_scan/adapters/base.py +162 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/external_starter.py +28 -5
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/generic_http.py +72 -22
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/llm_starter.py +26 -4
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot.py +35 -11
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/scan.py +392 -77
- agentproof_scan-0.1.4/agentproof_scan/victim_agent.py +82 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4/agentproof_scan.egg-info}/PKG-INFO +183 -20
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/SOURCES.txt +1 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/pyproject.toml +1 -1
- agentproof_scan-0.1.2/agentproof_scan/adapters/__init__.py +0 -41
- agentproof_scan-0.1.2/agentproof_scan/adapters/base.py +0 -29
- agentproof_scan-0.1.2/agentproof_scan/victim_agent.py +0 -55
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/LICENSE +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/MANIFEST.in +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_canary.py +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_defended_canary.py +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/simple_chatbot_hardened_canary.py +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/adapters/victim.py +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan/fingerprint.py +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/dependency_links.txt +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/entry_points.txt +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/requires.txt +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/agentproof_scan.egg-info/top_level.txt +0 -0
- {agentproof_scan-0.1.2 → agentproof_scan-0.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentproof-scan
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: CLI security scanner: probes AI-agent HTTP endpoints for secret/prompt leakage, with CI-gating exit codes.
|
|
5
5
|
Author: AgentProof
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -52,6 +52,59 @@ you can read the code and reproduce every number yourself.
|
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
+
## 📦 Prerequisites
|
|
56
|
+
|
|
57
|
+
If this is your first command-line tool, **the hard part is installing Python, not
|
|
58
|
+
installing this.** People get stuck there. That step isn't part of this tool, and it's
|
|
59
|
+
a normal place to get stuck.
|
|
60
|
+
|
|
61
|
+
**1. Check that you have Python 3.9 or newer.** One of these three works; which one
|
|
62
|
+
tells you how Python was installed on your machine:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
python --version
|
|
66
|
+
python3 --version
|
|
67
|
+
py -3 --version
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**2. Install.**
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pip install agentproof-scan
|
|
74
|
+
```
|
|
75
|
+
`pip: command not found` → `python -m pip install agentproof-scan`. On Windows without
|
|
76
|
+
Python, `py -m pip install agentproof-scan` triggers the Python Install Manager to fetch
|
|
77
|
+
Python for you. [VERIFIED: `py -m ...` installed Python 3.14.6 on Windows, owner, 2026-07-11.]
|
|
78
|
+
|
|
79
|
+
**3. Put your key in a `.env` file, with an editor — not a shell command.**
|
|
80
|
+
|
|
81
|
+
Open a new file called `.env` in the folder you'll run from, and put one line in it:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
GEMINI_API_KEY=your-real-key-here
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Save it. Get a free key at <https://aistudio.google.com/apikey>. `.env` is auto-loaded from
|
|
88
|
+
the directory you run in and is gitignored — never commit it.
|
|
89
|
+
|
|
90
|
+
> **Why an editor and not `echo '...' > .env`?** A shell command puts your key on the
|
|
91
|
+
> command line, and the shell saves that line to its **history** in plaintext
|
|
92
|
+
> (`~/.bash_history`, PowerShell's `ConsoleHost_history.txt`). A tool that detects leaked
|
|
93
|
+
> credentials should not teach you to write one to a file nobody thinks to check. An editor
|
|
94
|
+
> avoids the history entirely. (If you do use the shell, this tool does **not** scan shell
|
|
95
|
+
> history — that's on you to clear.)
|
|
96
|
+
|
|
97
|
+
> **On `.env` encoding.** The loader reads `.env` written as UTF-8, UTF-8 with BOM, and
|
|
98
|
+
> **UTF-16LE with BOM** (what PowerShell 5.1's `>` produces — [VERIFIED on a real PowerShell
|
|
99
|
+
> 5.1 file, owner, 2026-07-11]), and strips stray quotes `cmd.exe` leaves around the *key
|
|
100
|
+
> name*. **Don't quote the value** — `KEY="abc"` keeps the quotes literally.
|
|
101
|
+
|
|
102
|
+
**4. If the command isn't found:** `agentproof-scan: command not found` →
|
|
103
|
+
`python -m agentproof_scan` (Windows: `py -m agentproof_scan`) — same tool, same flags,
|
|
104
|
+
works when the console script isn't on PATH. [VERIFIED on Windows, owner, 2026-07-11.]
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
55
108
|
## 🚀 60-second Quick Start (no setup knowledge — just copy-paste)
|
|
56
109
|
|
|
57
110
|
`agentproof-scan` ships with a built-in **victim demo**: an intentionally leaky agent
|
|
@@ -62,19 +115,38 @@ backed by Google Gemini. You only need a free Gemini key to try it.
|
|
|
62
115
|
pip install agentproof-scan
|
|
63
116
|
|
|
64
117
|
# 2. Get a FREE Gemini key → https://aistudio.google.com/apikey
|
|
65
|
-
#
|
|
66
|
-
|
|
118
|
+
# Then create a file named .env in this folder (with an editor, not the shell —
|
|
119
|
+
# see Prerequisites) containing one line:
|
|
120
|
+
# GEMINI_API_KEY=your-real-key-here
|
|
67
121
|
|
|
68
122
|
# 3. Scan the built-in vulnerable demo agent
|
|
69
|
-
agentproof-scan #
|
|
70
|
-
agentproof-scan --stability
|
|
123
|
+
agentproof-scan # one pass = 15 probes = 15 API calls
|
|
124
|
+
agentproof-scan --stability 2 # 2 passes = 30 calls — more reliable (see notes)
|
|
71
125
|
```
|
|
72
126
|
|
|
73
127
|
A JSON report prints. If the demo leaked, you'll see a `leak_count` of 1 or more.
|
|
74
128
|
|
|
75
|
-
> ⚠️ **
|
|
76
|
-
|
|
77
|
-
>
|
|
129
|
+
> ⚠️ **On a free Gemini key, watch the request count.** Each pass sends **15 probes** = 15
|
|
130
|
+
> API calls; `--stability N` multiplies that (`--stability 5` = 75 calls, `--stability 10`
|
|
131
|
+
> = 150). The free tier has a per-minute quota, and hitting it is **not** a tool failure —
|
|
132
|
+
> the scanner exits `1` with `reason=rate_limit` and tells you to wait or lower
|
|
133
|
+
> `--stability`. Start with `--stability 2`; raise it once you know your quota headroom.
|
|
134
|
+
|
|
135
|
+
> ⚠️ **A zero you can trust.** A scan that never reached your agent, or was cut off
|
|
136
|
+
> partway (bad key, HTTP 500, timeout, rate limit), must not read as *"safe."* The scanner
|
|
137
|
+
> **refuses to report clean** unless every probe actually got an answer: otherwise it exits
|
|
138
|
+
> `1` and prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` on stderr (`rate_limit`,
|
|
139
|
+
> `auth_failed`, `http_status`, `incomplete_scan`, …). A leak it *did* find is still
|
|
140
|
+
> reported and, under `--fail-on-findings`, still exits `2` — **a partial scan may not
|
|
141
|
+
> claim clean, but it may claim what it found.** (Earlier versions, up to `0.1.3`, could
|
|
142
|
+
> print `leak_count: 0` and exit `0` here — that's the bug `0.1.4` closes.)
|
|
143
|
+
|
|
144
|
+
> **Why repeat with `--stability`?** A single run is non-deterministic — a leaky agent can
|
|
145
|
+
> still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating
|
|
146
|
+
> measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
|
|
147
|
+
> A probe that never got an answer shows `leak_rate: null` (**not** `0.0`) — "not asked" is
|
|
148
|
+
> not "didn't leak." ⚠ Each repeat costs `15 × N` API calls; on a free key start at
|
|
149
|
+
> `--stability 2` (see the request-count note above).
|
|
78
150
|
|
|
79
151
|
---
|
|
80
152
|
|
|
@@ -87,7 +159,7 @@ Two fields matter:
|
|
|
87
159
|
|
|
88
160
|
**Analogy:** `leak` = the guard handed over the vault key. `prompt_disclosure` = the guard didn't hand over the key, but read the security manual aloud. Both are bad; the first is worse.
|
|
89
161
|
|
|
90
|
-
**`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak
|
|
162
|
+
**`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak, over the runs that **got an answer**. For example `4/10 (0.4)` = 4 of 10 answered tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails. A probe that never got an answer (rate-limited, timed out) shows `leak_rate: null`, not `0.0` — the report separates *"didn't leak"* from *"wasn't asked."*
|
|
91
163
|
|
|
92
164
|
**Which secrets it recognizes:** the scanner looks for key shapes from major providers — OpenAI (including modern `sk-proj-` / `sk-svcacct-` / `sk-admin-` keys and the legacy `sk-` format), Anthropic, Google, AWS, GitHub, and xAI.
|
|
93
165
|
|
|
@@ -167,9 +239,10 @@ agentproof-scan \
|
|
|
167
239
|
**Needs auth?** Pass a header — but put only the **name** of an environment variable
|
|
168
240
|
in the flag, never the key itself:
|
|
169
241
|
|
|
242
|
+
Add the key to your `.env` (with an editor — same reason as the Quick Start: keep it out
|
|
243
|
+
of shell history) as `MY_AGENT_KEY=your-real-key`, then reference it **by name**:
|
|
244
|
+
|
|
170
245
|
```bash
|
|
171
|
-
# key lives in .env (gitignored); the flag references it by name
|
|
172
|
-
echo 'MY_AGENT_KEY=sk-your-real-key' >> .env
|
|
173
246
|
agentproof-scan --url https://my-agent.example.com/chat \
|
|
174
247
|
--prompt-field message --response-field reply \
|
|
175
248
|
--auth-header "Authorization=Bearer {MY_AGENT_KEY}"
|
|
@@ -260,16 +333,103 @@ jobs:
|
|
|
260
333
|
|
|
261
334
|
*(Also at [`examples/ci/agentproof.yml`](https://github.com/ghkfuddl1327-wq/agentproof/blob/main/examples/ci/agentproof.yml) — copy it, don't clone the repo.)*
|
|
262
335
|
|
|
263
|
-
**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
336
|
+
**Keep the `AGENT_URL` guard anyway.** An unset secret is an empty string, and the
|
|
337
|
+
guard turns that into a clear "secret not set" failure. Since `0.1.3` an empty `--url`
|
|
338
|
+
already exits `1` on its own (`reason=missing_url`) rather than scanning anything — so
|
|
339
|
+
a missing `AGENT_URL` fails the build as *"the scan did not run,"* not as *"your agent
|
|
340
|
+
leaked."* The guard just makes that reason obvious in the log. Forked pull requests get
|
|
341
|
+
no secrets, so they hit this path — and a `1` there is correct: nothing was scanned,
|
|
342
|
+
and nothing is claimed clean.
|
|
268
343
|
|
|
269
344
|
If your agent leaks a secret, `--fail-on-findings` exits non-zero and the check goes
|
|
270
345
|
red. By default it fails on **leaked secrets only** (not softer prompt-disclosure
|
|
271
346
|
signals); add `--fail-on any` to gate on those too. Scan only agents you own.
|
|
272
347
|
|
|
348
|
+
**Exit codes** — three states, never two:
|
|
349
|
+
|
|
350
|
+
| Code | Meaning | When |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| `0` | Clean | Every probe reached your agent, nothing leaked |
|
|
353
|
+
| `1` | **The scan did not run** | Missing/placeholder key, HTTP error, timeout, bad flag. Never means "safe". Prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` to stderr |
|
|
354
|
+
| `2` | Findings | The agent leaked (requires `--fail-on-findings`) |
|
|
355
|
+
|
|
356
|
+
`1` and `2` are different on purpose: a build that fails because *nobody could ask the
|
|
357
|
+
agent* must not be read as *the agent leaked*, and neither may be read as *safe*.
|
|
358
|
+
|
|
359
|
+
> **This table was not true before `0.1.3`.** It was written as documentation of intent,
|
|
360
|
+
> but the code never matched it: an invalid CLI flag exited `2` (argparse's default),
|
|
361
|
+
> which a CI gate reads as *"your agent leaked a secret."* And a scan that never reached
|
|
362
|
+
> the agent exited `0`, which reads as *"safe."* Both are fixed in `0.1.3`. Changing the
|
|
363
|
+
> flag error from `2` to `1` is not a change to the contract — it is the first release in
|
|
364
|
+
> which the contract is true. Every non-zero exit now also prints
|
|
365
|
+
> `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` (`missing_env`, `placeholder_key`,
|
|
366
|
+
> `http_status`, `timeout`, `nonzero_exit`, `missing_url`, `auth_missing_env`,
|
|
367
|
+
> `usage_error`, …) so CI can tell *which* guard fired. The exit code is the contract;
|
|
368
|
+
> the slug is the evidence.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Verification scope
|
|
373
|
+
|
|
374
|
+
All test data in this repository comes from **Linux containers**.
|
|
375
|
+
|
|
376
|
+
**Verified** — 9 cells, live PyPI install (not local build):
|
|
377
|
+
Python 3.9 / 3.11 / 3.13 × venv / system / pipx
|
|
378
|
+
|
|
379
|
+
**Not verified:**
|
|
380
|
+
|
|
381
|
+
| Environment | Status |
|
|
382
|
+
|---|---|
|
|
383
|
+
| Windows (PowerShell 5.1 / pwsh 7 / cmd) | Not verified. CI coverage planned. |
|
|
384
|
+
| macOS | Not verified. CI coverage planned. |
|
|
385
|
+
| Linux, installed on host (not container) | Not verified. |
|
|
386
|
+
| GitHub Codespaces, as a new user | Not verified. We develop there; we have never walked the first-run path. |
|
|
387
|
+
| Python installation itself (PATH, python.org / MS Store / pyenv) | **Cannot be covered by CI.** Runners ship with Python pre-installed. |
|
|
388
|
+
|
|
389
|
+
The last row will not be closed by automation. If you hit friction installing
|
|
390
|
+
Python itself, an issue is the only way that row changes.
|
|
391
|
+
|
|
392
|
+
`0.1.4` handles UTF-16LE+BOM `.env` files (byte-level test).
|
|
393
|
+
This is **not** a claim that the tool passes on Windows PowerShell 5.1.
|
|
394
|
+
It is a claim about the bytes.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## If you don't have Python
|
|
399
|
+
|
|
400
|
+
GitHub Codespaces gives you a Linux container in the browser.
|
|
401
|
+
Requires only a GitHub account. No local Python installation.
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
pip install agentproof-scan
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
⚠ **Before you do this, read the next section.**
|
|
408
|
+
Codespaces is not verified as a new-user path (see table above),
|
|
409
|
+
and running this tool means putting a provider API key into a cloud VM.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Handling your API key ⚠
|
|
414
|
+
|
|
415
|
+
This tool needs a live provider key to call your agent.
|
|
416
|
+
Wherever you run it, that key is exposed to that environment.
|
|
417
|
+
|
|
418
|
+
- Use a **scoped, low-quota, disposable key**. Not your production key.
|
|
419
|
+
- Set a hard spend cap before you start. Budget *alerts* are not caps.
|
|
420
|
+
- **Revoke the key when you are done.**
|
|
421
|
+
- In Codespaces: use a Codespaces secret, not a committed `.env`.
|
|
422
|
+
Never commit `.env`. It is gitignored — do not override that.
|
|
423
|
+
- If you fork this repo, your fork's Codespace inherits nothing of ours.
|
|
424
|
+
Your key is yours to manage.
|
|
425
|
+
|
|
426
|
+
A Codespaces secret arrives as an environment variable, and a real environment
|
|
427
|
+
variable always beats a `.env` file — so the scanner runs with no `.env` at all.
|
|
428
|
+
|
|
429
|
+
This tool detects leaks. It does not prevent them.
|
|
430
|
+
Scoping, rate limits, and hard spend caps do that. See
|
|
431
|
+
[What it catches — and what it doesn't](#what-it-catches--and-what-it-doesnt-plainly).
|
|
432
|
+
|
|
273
433
|
---
|
|
274
434
|
|
|
275
435
|
## ❓ Stuck? (no experience needed — your escape hatch)
|
|
@@ -290,10 +450,13 @@ If any step is confusing, paste this into an AI assistant and follow along:
|
|
|
290
450
|
|
|
291
451
|
Early work in progress. This tool grew out of red-team probing experiments and is expanding toward broader pre-deployment credential-exposure detection. The detection rule and the cross-model numbers are still being validated — **expect changes**, and if you can break something we marked as working, please open an issue.
|
|
292
452
|
|
|
293
|
-
**
|
|
453
|
+
**Invalid keys, since `0.1.3`:** a present-but-invalid key (wrong or expired) no longer
|
|
454
|
+
produces a misleading `0`. The provider returns an HTTP error, the scanner treats that
|
|
455
|
+
as *the scan did not run* — exit `1`, `reason=http_status` — and refuses to report clean.
|
|
456
|
+
Earlier versions did report `0` here; that was the bug `0.1.3` closes.
|
|
294
457
|
|
|
295
458
|
**Not yet in this release:** wider credential-type coverage (Stripe, Slack, JWT, PEM,
|
|
296
|
-
SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.
|
|
459
|
+
SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.4` ships only the six
|
|
297
460
|
families listed under [What it catches](#what-it-catches--and-what-it-doesnt-plainly).
|
|
298
461
|
It lands in a later release rather than being advertised here before you can run it.
|
|
299
462
|
|
|
@@ -353,7 +516,7 @@ into a wall of text.
|
|
|
353
516
|
## What it catches — and what it doesn't (plainly)
|
|
354
517
|
|
|
355
518
|
**It catches:** a set list of credential types, matched by their shape. As of
|
|
356
|
-
`0.1.
|
|
519
|
+
`0.1.4` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
|
|
357
520
|
Matching holds up whether the secret is in plain text or JSON, across different
|
|
358
521
|
languages, and in the answer or the reasoning — for the types it knows.
|
|
359
522
|
|
|
@@ -363,7 +526,7 @@ and docs don't set off false alarms.
|
|
|
363
526
|
|
|
364
527
|
**It doesn't catch:**
|
|
365
528
|
- **Credential types outside those six** — Stripe, Slack, JWTs, PEM private keys,
|
|
366
|
-
SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.
|
|
529
|
+
SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.4`. Broader
|
|
367
530
|
type coverage is built and tested but not yet released here (see Status).
|
|
368
531
|
- **Secrets with no tell-tale prefix** — e.g. a database password buried in a
|
|
369
532
|
`postgres://…` URL. A real limit of shape-matching.
|
|
@@ -30,6 +30,59 @@ you can read the code and reproduce every number yourself.
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## 📦 Prerequisites
|
|
34
|
+
|
|
35
|
+
If this is your first command-line tool, **the hard part is installing Python, not
|
|
36
|
+
installing this.** People get stuck there. That step isn't part of this tool, and it's
|
|
37
|
+
a normal place to get stuck.
|
|
38
|
+
|
|
39
|
+
**1. Check that you have Python 3.9 or newer.** One of these three works; which one
|
|
40
|
+
tells you how Python was installed on your machine:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
python --version
|
|
44
|
+
python3 --version
|
|
45
|
+
py -3 --version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**2. Install.**
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install agentproof-scan
|
|
52
|
+
```
|
|
53
|
+
`pip: command not found` → `python -m pip install agentproof-scan`. On Windows without
|
|
54
|
+
Python, `py -m pip install agentproof-scan` triggers the Python Install Manager to fetch
|
|
55
|
+
Python for you. [VERIFIED: `py -m ...` installed Python 3.14.6 on Windows, owner, 2026-07-11.]
|
|
56
|
+
|
|
57
|
+
**3. Put your key in a `.env` file, with an editor — not a shell command.**
|
|
58
|
+
|
|
59
|
+
Open a new file called `.env` in the folder you'll run from, and put one line in it:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
GEMINI_API_KEY=your-real-key-here
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Save it. Get a free key at <https://aistudio.google.com/apikey>. `.env` is auto-loaded from
|
|
66
|
+
the directory you run in and is gitignored — never commit it.
|
|
67
|
+
|
|
68
|
+
> **Why an editor and not `echo '...' > .env`?** A shell command puts your key on the
|
|
69
|
+
> command line, and the shell saves that line to its **history** in plaintext
|
|
70
|
+
> (`~/.bash_history`, PowerShell's `ConsoleHost_history.txt`). A tool that detects leaked
|
|
71
|
+
> credentials should not teach you to write one to a file nobody thinks to check. An editor
|
|
72
|
+
> avoids the history entirely. (If you do use the shell, this tool does **not** scan shell
|
|
73
|
+
> history — that's on you to clear.)
|
|
74
|
+
|
|
75
|
+
> **On `.env` encoding.** The loader reads `.env` written as UTF-8, UTF-8 with BOM, and
|
|
76
|
+
> **UTF-16LE with BOM** (what PowerShell 5.1's `>` produces — [VERIFIED on a real PowerShell
|
|
77
|
+
> 5.1 file, owner, 2026-07-11]), and strips stray quotes `cmd.exe` leaves around the *key
|
|
78
|
+
> name*. **Don't quote the value** — `KEY="abc"` keeps the quotes literally.
|
|
79
|
+
|
|
80
|
+
**4. If the command isn't found:** `agentproof-scan: command not found` →
|
|
81
|
+
`python -m agentproof_scan` (Windows: `py -m agentproof_scan`) — same tool, same flags,
|
|
82
|
+
works when the console script isn't on PATH. [VERIFIED on Windows, owner, 2026-07-11.]
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
33
86
|
## 🚀 60-second Quick Start (no setup knowledge — just copy-paste)
|
|
34
87
|
|
|
35
88
|
`agentproof-scan` ships with a built-in **victim demo**: an intentionally leaky agent
|
|
@@ -40,19 +93,38 @@ backed by Google Gemini. You only need a free Gemini key to try it.
|
|
|
40
93
|
pip install agentproof-scan
|
|
41
94
|
|
|
42
95
|
# 2. Get a FREE Gemini key → https://aistudio.google.com/apikey
|
|
43
|
-
#
|
|
44
|
-
|
|
96
|
+
# Then create a file named .env in this folder (with an editor, not the shell —
|
|
97
|
+
# see Prerequisites) containing one line:
|
|
98
|
+
# GEMINI_API_KEY=your-real-key-here
|
|
45
99
|
|
|
46
100
|
# 3. Scan the built-in vulnerable demo agent
|
|
47
|
-
agentproof-scan #
|
|
48
|
-
agentproof-scan --stability
|
|
101
|
+
agentproof-scan # one pass = 15 probes = 15 API calls
|
|
102
|
+
agentproof-scan --stability 2 # 2 passes = 30 calls — more reliable (see notes)
|
|
49
103
|
```
|
|
50
104
|
|
|
51
105
|
A JSON report prints. If the demo leaked, you'll see a `leak_count` of 1 or more.
|
|
52
106
|
|
|
53
|
-
> ⚠️ **
|
|
54
|
-
|
|
55
|
-
>
|
|
107
|
+
> ⚠️ **On a free Gemini key, watch the request count.** Each pass sends **15 probes** = 15
|
|
108
|
+
> API calls; `--stability N` multiplies that (`--stability 5` = 75 calls, `--stability 10`
|
|
109
|
+
> = 150). The free tier has a per-minute quota, and hitting it is **not** a tool failure —
|
|
110
|
+
> the scanner exits `1` with `reason=rate_limit` and tells you to wait or lower
|
|
111
|
+
> `--stability`. Start with `--stability 2`; raise it once you know your quota headroom.
|
|
112
|
+
|
|
113
|
+
> ⚠️ **A zero you can trust.** A scan that never reached your agent, or was cut off
|
|
114
|
+
> partway (bad key, HTTP 500, timeout, rate limit), must not read as *"safe."* The scanner
|
|
115
|
+
> **refuses to report clean** unless every probe actually got an answer: otherwise it exits
|
|
116
|
+
> `1` and prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` on stderr (`rate_limit`,
|
|
117
|
+
> `auth_failed`, `http_status`, `incomplete_scan`, …). A leak it *did* find is still
|
|
118
|
+
> reported and, under `--fail-on-findings`, still exits `2` — **a partial scan may not
|
|
119
|
+
> claim clean, but it may claim what it found.** (Earlier versions, up to `0.1.3`, could
|
|
120
|
+
> print `leak_count: 0` and exit `0` here — that's the bug `0.1.4` closes.)
|
|
121
|
+
|
|
122
|
+
> **Why repeat with `--stability`?** A single run is non-deterministic — a leaky agent can
|
|
123
|
+
> still answer "safely" on any one try, so a one-shot scan might read `0` by luck. Repeating
|
|
124
|
+
> measures *how often* it leaks (`leak_rate`) and is the reliable way to read the verdict.
|
|
125
|
+
> A probe that never got an answer shows `leak_rate: null` (**not** `0.0`) — "not asked" is
|
|
126
|
+
> not "didn't leak." ⚠ Each repeat costs `15 × N` API calls; on a free key start at
|
|
127
|
+
> `--stability 2` (see the request-count note above).
|
|
56
128
|
|
|
57
129
|
---
|
|
58
130
|
|
|
@@ -65,7 +137,7 @@ Two fields matter:
|
|
|
65
137
|
|
|
66
138
|
**Analogy:** `leak` = the guard handed over the vault key. `prompt_disclosure` = the guard didn't hand over the key, but read the security manual aloud. Both are bad; the first is worse.
|
|
67
139
|
|
|
68
|
-
**`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak
|
|
140
|
+
**`leak_rate`** (in repeat/stability mode) = how often a probe pulled a leak, over the runs that **got an answer**. For example `4/10 (0.4)` = 4 of 10 answered tries leaked. A flaky leak is still a leak — repetition shows how *reliably* an agent fails. A probe that never got an answer (rate-limited, timed out) shows `leak_rate: null`, not `0.0` — the report separates *"didn't leak"* from *"wasn't asked."*
|
|
69
141
|
|
|
70
142
|
**Which secrets it recognizes:** the scanner looks for key shapes from major providers — OpenAI (including modern `sk-proj-` / `sk-svcacct-` / `sk-admin-` keys and the legacy `sk-` format), Anthropic, Google, AWS, GitHub, and xAI.
|
|
71
143
|
|
|
@@ -145,9 +217,10 @@ agentproof-scan \
|
|
|
145
217
|
**Needs auth?** Pass a header — but put only the **name** of an environment variable
|
|
146
218
|
in the flag, never the key itself:
|
|
147
219
|
|
|
220
|
+
Add the key to your `.env` (with an editor — same reason as the Quick Start: keep it out
|
|
221
|
+
of shell history) as `MY_AGENT_KEY=your-real-key`, then reference it **by name**:
|
|
222
|
+
|
|
148
223
|
```bash
|
|
149
|
-
# key lives in .env (gitignored); the flag references it by name
|
|
150
|
-
echo 'MY_AGENT_KEY=sk-your-real-key' >> .env
|
|
151
224
|
agentproof-scan --url https://my-agent.example.com/chat \
|
|
152
225
|
--prompt-field message --response-field reply \
|
|
153
226
|
--auth-header "Authorization=Bearer {MY_AGENT_KEY}"
|
|
@@ -238,16 +311,103 @@ jobs:
|
|
|
238
311
|
|
|
239
312
|
*(Also at [`examples/ci/agentproof.yml`](https://github.com/ghkfuddl1327-wq/agentproof/blob/main/examples/ci/agentproof.yml) — copy it, don't clone the repo.)*
|
|
240
313
|
|
|
241
|
-
**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
314
|
+
**Keep the `AGENT_URL` guard anyway.** An unset secret is an empty string, and the
|
|
315
|
+
guard turns that into a clear "secret not set" failure. Since `0.1.3` an empty `--url`
|
|
316
|
+
already exits `1` on its own (`reason=missing_url`) rather than scanning anything — so
|
|
317
|
+
a missing `AGENT_URL` fails the build as *"the scan did not run,"* not as *"your agent
|
|
318
|
+
leaked."* The guard just makes that reason obvious in the log. Forked pull requests get
|
|
319
|
+
no secrets, so they hit this path — and a `1` there is correct: nothing was scanned,
|
|
320
|
+
and nothing is claimed clean.
|
|
246
321
|
|
|
247
322
|
If your agent leaks a secret, `--fail-on-findings` exits non-zero and the check goes
|
|
248
323
|
red. By default it fails on **leaked secrets only** (not softer prompt-disclosure
|
|
249
324
|
signals); add `--fail-on any` to gate on those too. Scan only agents you own.
|
|
250
325
|
|
|
326
|
+
**Exit codes** — three states, never two:
|
|
327
|
+
|
|
328
|
+
| Code | Meaning | When |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| `0` | Clean | Every probe reached your agent, nothing leaked |
|
|
331
|
+
| `1` | **The scan did not run** | Missing/placeholder key, HTTP error, timeout, bad flag. Never means "safe". Prints `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` to stderr |
|
|
332
|
+
| `2` | Findings | The agent leaked (requires `--fail-on-findings`) |
|
|
333
|
+
|
|
334
|
+
`1` and `2` are different on purpose: a build that fails because *nobody could ask the
|
|
335
|
+
agent* must not be read as *the agent leaked*, and neither may be read as *safe*.
|
|
336
|
+
|
|
337
|
+
> **This table was not true before `0.1.3`.** It was written as documentation of intent,
|
|
338
|
+
> but the code never matched it: an invalid CLI flag exited `2` (argparse's default),
|
|
339
|
+
> which a CI gate reads as *"your agent leaked a secret."* And a scan that never reached
|
|
340
|
+
> the agent exited `0`, which reads as *"safe."* Both are fixed in `0.1.3`. Changing the
|
|
341
|
+
> flag error from `2` to `1` is not a change to the contract — it is the first release in
|
|
342
|
+
> which the contract is true. Every non-zero exit now also prints
|
|
343
|
+
> `AGENTPROOF_SCAN_DID_NOT_RUN reason=<slug>` (`missing_env`, `placeholder_key`,
|
|
344
|
+
> `http_status`, `timeout`, `nonzero_exit`, `missing_url`, `auth_missing_env`,
|
|
345
|
+
> `usage_error`, …) so CI can tell *which* guard fired. The exit code is the contract;
|
|
346
|
+
> the slug is the evidence.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Verification scope
|
|
351
|
+
|
|
352
|
+
All test data in this repository comes from **Linux containers**.
|
|
353
|
+
|
|
354
|
+
**Verified** — 9 cells, live PyPI install (not local build):
|
|
355
|
+
Python 3.9 / 3.11 / 3.13 × venv / system / pipx
|
|
356
|
+
|
|
357
|
+
**Not verified:**
|
|
358
|
+
|
|
359
|
+
| Environment | Status |
|
|
360
|
+
|---|---|
|
|
361
|
+
| Windows (PowerShell 5.1 / pwsh 7 / cmd) | Not verified. CI coverage planned. |
|
|
362
|
+
| macOS | Not verified. CI coverage planned. |
|
|
363
|
+
| Linux, installed on host (not container) | Not verified. |
|
|
364
|
+
| GitHub Codespaces, as a new user | Not verified. We develop there; we have never walked the first-run path. |
|
|
365
|
+
| Python installation itself (PATH, python.org / MS Store / pyenv) | **Cannot be covered by CI.** Runners ship with Python pre-installed. |
|
|
366
|
+
|
|
367
|
+
The last row will not be closed by automation. If you hit friction installing
|
|
368
|
+
Python itself, an issue is the only way that row changes.
|
|
369
|
+
|
|
370
|
+
`0.1.4` handles UTF-16LE+BOM `.env` files (byte-level test).
|
|
371
|
+
This is **not** a claim that the tool passes on Windows PowerShell 5.1.
|
|
372
|
+
It is a claim about the bytes.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## If you don't have Python
|
|
377
|
+
|
|
378
|
+
GitHub Codespaces gives you a Linux container in the browser.
|
|
379
|
+
Requires only a GitHub account. No local Python installation.
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
pip install agentproof-scan
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
⚠ **Before you do this, read the next section.**
|
|
386
|
+
Codespaces is not verified as a new-user path (see table above),
|
|
387
|
+
and running this tool means putting a provider API key into a cloud VM.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Handling your API key ⚠
|
|
392
|
+
|
|
393
|
+
This tool needs a live provider key to call your agent.
|
|
394
|
+
Wherever you run it, that key is exposed to that environment.
|
|
395
|
+
|
|
396
|
+
- Use a **scoped, low-quota, disposable key**. Not your production key.
|
|
397
|
+
- Set a hard spend cap before you start. Budget *alerts* are not caps.
|
|
398
|
+
- **Revoke the key when you are done.**
|
|
399
|
+
- In Codespaces: use a Codespaces secret, not a committed `.env`.
|
|
400
|
+
Never commit `.env`. It is gitignored — do not override that.
|
|
401
|
+
- If you fork this repo, your fork's Codespace inherits nothing of ours.
|
|
402
|
+
Your key is yours to manage.
|
|
403
|
+
|
|
404
|
+
A Codespaces secret arrives as an environment variable, and a real environment
|
|
405
|
+
variable always beats a `.env` file — so the scanner runs with no `.env` at all.
|
|
406
|
+
|
|
407
|
+
This tool detects leaks. It does not prevent them.
|
|
408
|
+
Scoping, rate limits, and hard spend caps do that. See
|
|
409
|
+
[What it catches — and what it doesn't](#what-it-catches--and-what-it-doesnt-plainly).
|
|
410
|
+
|
|
251
411
|
---
|
|
252
412
|
|
|
253
413
|
## ❓ Stuck? (no experience needed — your escape hatch)
|
|
@@ -268,10 +428,13 @@ If any step is confusing, paste this into an AI assistant and follow along:
|
|
|
268
428
|
|
|
269
429
|
Early work in progress. This tool grew out of red-team probing experiments and is expanding toward broader pre-deployment credential-exposure detection. The detection rule and the cross-model numbers are still being validated — **expect changes**, and if you can break something we marked as working, please open an issue.
|
|
270
430
|
|
|
271
|
-
**
|
|
431
|
+
**Invalid keys, since `0.1.3`:** a present-but-invalid key (wrong or expired) no longer
|
|
432
|
+
produces a misleading `0`. The provider returns an HTTP error, the scanner treats that
|
|
433
|
+
as *the scan did not run* — exit `1`, `reason=http_status` — and refuses to report clean.
|
|
434
|
+
Earlier versions did report `0` here; that was the bug `0.1.3` closes.
|
|
272
435
|
|
|
273
436
|
**Not yet in this release:** wider credential-type coverage (Stripe, Slack, JWT, PEM,
|
|
274
|
-
SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.
|
|
437
|
+
SendGrid, Twilio, npm, …) is implemented and tested, but `0.1.4` ships only the six
|
|
275
438
|
families listed under [What it catches](#what-it-catches--and-what-it-doesnt-plainly).
|
|
276
439
|
It lands in a later release rather than being advertised here before you can run it.
|
|
277
440
|
|
|
@@ -331,7 +494,7 @@ into a wall of text.
|
|
|
331
494
|
## What it catches — and what it doesn't (plainly)
|
|
332
495
|
|
|
333
496
|
**It catches:** a set list of credential types, matched by their shape. As of
|
|
334
|
-
`0.1.
|
|
497
|
+
`0.1.4` that's **six families** — OpenAI, Anthropic, Google, AWS, GitHub, and xAI.
|
|
335
498
|
Matching holds up whether the secret is in plain text or JSON, across different
|
|
336
499
|
languages, and in the answer or the reasoning — for the types it knows.
|
|
337
500
|
|
|
@@ -341,7 +504,7 @@ and docs don't set off false alarms.
|
|
|
341
504
|
|
|
342
505
|
**It doesn't catch:**
|
|
343
506
|
- **Credential types outside those six** — Stripe, Slack, JWTs, PEM private keys,
|
|
344
|
-
SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.
|
|
507
|
+
SendGrid, Twilio, npm tokens and others are **not** matched in `0.1.4`. Broader
|
|
345
508
|
type coverage is built and tested but not yet released here (see Status).
|
|
346
509
|
- **Secrets with no tell-tale prefix** — e.g. a database password buried in a
|
|
347
510
|
`postgres://…` URL. A real limit of shape-matching.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""adapters — 스캐너가 다양한 타깃 에이전트를 동일 인터페이스로 검사하기 위한 어댑터 패키지.
|
|
2
|
+
|
|
3
|
+
import 시 .env(KEY=VALUE)를 os.environ에 로드한다. 두 위치를 본다:
|
|
4
|
+
(1) 소스 트리 루트(개발 시 clone 루트) — 기존 동작 유지,
|
|
5
|
+
(2) 현재 작업 디렉터리 ./.env — pip 설치 후 사용자가 CLI 를 실행하는 곳.
|
|
6
|
+
Claude Code의 Bash 셸은 사용자의 대화형 터미널 export를 상속하지 않으므로,
|
|
7
|
+
키는 .env에 적어두면 어댑터가 환경변수로 읽어 쓴다(.env는 .gitignore 대상).
|
|
8
|
+
설치판에서는 (1)이 site-packages 내부라 무의미 → (2)가 실제 동작 경로다.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _decode_dotenv(raw):
|
|
15
|
+
""".env 바이트를 텍스트로. 셸마다 디스크에 남기는 바이트가 다르다.
|
|
16
|
+
|
|
17
|
+
- bash/zsh : UTF-8, LF
|
|
18
|
+
- pwsh 7 : UTF-8(BOM 없음), CRLF
|
|
19
|
+
- PowerShell 5.1: `>` 가 Out-File 이고 기본 인코딩이 Unicode = **UTF-16LE + BOM**
|
|
20
|
+
- cmd.exe : UTF-8, CRLF
|
|
21
|
+
|
|
22
|
+
utf-8-sig 는 UTF-8 BOM 을 벗기고, 실패하면(0xff 0xfe 로 시작하는 UTF-16LE 등)
|
|
23
|
+
utf-16 으로 재시도한다(BOM 으로 바이트 순서 판별).
|
|
24
|
+
|
|
25
|
+
주장할 수 있는 것은 "이 바이트를 읽는다"이지 "PowerShell 5.1 을 통과한다"가 아니다.
|
|
26
|
+
실 Windows 의 PATH/pip/console-script 해석은 여전히 미검증이다.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return raw.decode("utf-8-sig")
|
|
30
|
+
except UnicodeDecodeError:
|
|
31
|
+
return raw.decode("utf-16")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _unwrap_whole_assignment(line):
|
|
35
|
+
"""`'KEY=VALUE'` 처럼 **할당 전체**를 감싼 따옴표 짝만 벗긴다.
|
|
36
|
+
|
|
37
|
+
cmd.exe 는 `echo 'K=V' > .env` 의 작은따옴표를 특수문자가 아니라 리터럴로
|
|
38
|
+
파일에 남긴다. 그러면 키가 `'GEMINI_API_KEY`, 값이 `V'` 가 되어 둘 다 오염된다.
|
|
39
|
+
|
|
40
|
+
**값 안의 따옴표는 건드리지 않는다.** `KEY='v'` 는 값이 `'v'` 로 남는다.
|
|
41
|
+
"""
|
|
42
|
+
if len(line) >= 2 and line[0] == line[-1] and line[0] in "\"'" and "=" in line:
|
|
43
|
+
return line[1:-1]
|
|
44
|
+
return line
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_dotenv(path):
|
|
48
|
+
if not os.path.isfile(path):
|
|
49
|
+
return
|
|
50
|
+
with open(path, "rb") as f:
|
|
51
|
+
text = _decode_dotenv(f.read())
|
|
52
|
+
for line in text.splitlines():
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
55
|
+
continue
|
|
56
|
+
line = _unwrap_whole_assignment(line)
|
|
57
|
+
key, _, value = line.partition("=")
|
|
58
|
+
# 키: 감싸는 따옴표를 벗긴다. 환경변수 이름에 따옴표가 들어갈 일은 없고,
|
|
59
|
+
# 오염되면 키가 영영 안 잡힌다(cmd.exe 경로).
|
|
60
|
+
key = key.strip().strip('"').strip("'").strip()
|
|
61
|
+
# 값: **절대 벗기지 않는다.** 값은 자격증명 리터럴이고 우리 탐지는 리터럴
|
|
62
|
+
# 매칭이다. 로더가 값을 변형하면 파일의 문자열과 메모리의 문자열이
|
|
63
|
+
# 달라지고, FP/FN 이 우리가 통제하지 못하는 지점에서 태어난다.
|
|
64
|
+
# 로더의 관용은 키 이름까지다 — 탐지 표면에는 닿지 않는다.
|
|
65
|
+
value = value.strip()
|
|
66
|
+
if not key:
|
|
67
|
+
continue
|
|
68
|
+
# 이미 환경에 있으면 덮어쓰지 않는다(실제 환경변수 우선).
|
|
69
|
+
os.environ.setdefault(key, value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_dotenv():
|
|
73
|
+
# (1) 소스 트리 루트(= 이 파일에서 두 단계 위), (2) 현재 작업 디렉터리.
|
|
74
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
75
|
+
seen = set()
|
|
76
|
+
for path in (os.path.join(root, ".env"), os.path.join(os.getcwd(), ".env")):
|
|
77
|
+
rp = os.path.realpath(path)
|
|
78
|
+
if rp in seen:
|
|
79
|
+
continue
|
|
80
|
+
seen.add(rp)
|
|
81
|
+
_parse_dotenv(path)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_load_dotenv()
|