residoo 0.8.0 → 0.8.2
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.
- package/README.md +78 -536
- package/package.json +7 -3
- package/src/guard.js +43 -9
- package/src/patterns.js +9 -4
package/README.md
CHANGED
|
@@ -43,16 +43,16 @@ scanned here left your machine; residoo makes no network calls.
|
|
|
43
43
|
|
|
44
44
|
That's one snapshot. `residoo watch` runs the same engine continuously and
|
|
45
45
|
alerts the moment a new secret lands, instead of waiting for you to
|
|
46
|
-
remember to scan again
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
remember to scan again. `residoo mcp` lets Claude Code query findings
|
|
47
|
+
conversationally. `residoo cred` removes the reason a credential gets
|
|
48
|
+
pasted into chat in the first place: store it once in your OS keychain,
|
|
49
|
+
run a command with it injected as an environment variable, never typed
|
|
50
|
+
into the conversation at all — which also means a long session compacting
|
|
51
|
+
away the exact value you pasted days ago can't force you to paste it
|
|
52
|
+
again, since there's nothing to lose. `residoo guard` blocks an obviously
|
|
53
|
+
sensitive file read before it happens (100% recall, 0% false positives on
|
|
54
|
+
its own [scored 81-case corpus](bench/guard/RESULTS.md)). All four are
|
|
55
|
+
covered in [docs/features.md](docs/features.md).
|
|
56
56
|
|
|
57
57
|
> [!NOTE]
|
|
58
58
|
> gitleaks and trufflehog scan **commits**. residoo scans the **conversation
|
|
@@ -61,45 +61,39 @@ watching 43 sources, 118 files · polling every 5s
|
|
|
61
61
|
> trufflehog/betterleaks' verification postures, in
|
|
62
62
|
> [docs/comparison.md](docs/comparison.md).
|
|
63
63
|
|
|
64
|
-
Scan and watch tell you what already leaked. The most common way a NEW
|
|
65
|
-
leak happens is pasting a key into the chat so Claude can use it, which
|
|
66
|
-
then sits in that conversation's transcript forever, the exact thing scan
|
|
67
|
-
exists to catch in the first place. `residoo cred` closes that loop: store
|
|
68
|
-
a credential once in your OS keychain, then let Claude run a command with
|
|
69
|
-
it injected as an environment variable, never pasted into the chat, never
|
|
70
|
-
written into a script (see
|
|
71
|
-
[Cred: run commands with injected credentials](#cred-run-commands-with-injected-credentials)):
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
$ residoo cred set aws-prod --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY
|
|
75
|
-
Value for AWS_ACCESS_KEY_ID (input hidden):
|
|
76
|
-
Value for AWS_SECRET_ACCESS_KEY (input hidden):
|
|
77
|
-
Stored credential "aws-prod" (2 env vars: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
|
|
78
|
-
|
|
79
|
-
$ residoo cred run aws-prod -- aws s3 ls
|
|
80
|
-
exit 0 (succeeded). stdout: 3 line(s), stderr: 0 line(s).
|
|
81
|
-
Command output is never shown by design, only exit status and line counts.
|
|
82
|
-
```
|
|
83
|
-
|
|
84
64
|
## Benchmark: measured, not claimed
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
pattern-true corpus
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
66
|
+
Scored #1 of 8 real competing tools on a reproducible, synthetic-but-
|
|
67
|
+
pattern-true corpus, with live egress monitoring so "no network calls" is
|
|
68
|
+
observed, not just documented. All 8, not just the closest one:
|
|
69
|
+
|
|
70
|
+
| tool | distinct credentials found | precision | egress during the scan |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| **residoo** | **45/45 (100%)** | **100%** | **none-observed** |
|
|
73
|
+
| agentsweep | 33/42 (79%) | 100% | none-observed |
|
|
74
|
+
| gitleaks | 32/45 (71%) | 100% | none-observed |
|
|
75
|
+
| betterleaks | 32/45 (71%) | 100% | none-observed |
|
|
76
|
+
| whatileaked | 28/42 (67%) | 100% | none-observed |
|
|
77
|
+
| kingfisher | 29/45 (64%) | 100% | attempts calls in default mode |
|
|
78
|
+
| trufflehog | 29/45 (64%) | 97% | attempts calls in default mode |
|
|
79
|
+
| detect-secrets | 25/45 (56%) | 2% | attempts calls in default mode |
|
|
80
|
+
|
|
81
|
+
"none-observed" is a measured result, not a default assumption: every run
|
|
82
|
+
sits under a live proxy trap and process-tree polling, and a deliberate
|
|
83
|
+
canary connection is fired and confirmed caught *before* each real
|
|
84
|
+
benchmark run, specifically so a clean result is falsifiable evidence, not
|
|
85
|
+
silence. The 3 rows with real outbound calls prove the monitor was
|
|
86
|
+
watching them too — kingfisher, trufflehog, and detect-secrets each ship
|
|
87
|
+
an *optional* live-verification feature (checking a found secret against
|
|
88
|
+
the vendor's own API), scored here in their documented offline mode for a
|
|
89
|
+
fair recall comparison, with their default mode's real connection
|
|
90
|
+
attempts reported factually rather than hidden.
|
|
91
|
+
|
|
92
|
+
GitGuardian's `ggshield` is documented, not scored: it refuses to run
|
|
93
|
+
without a server account, so there's no local result to measure. Published
|
|
94
|
+
while losing rows, then fixed in public against the classes it was losing
|
|
95
|
+
— full methodology, every dated rerun, and how to reproduce it yourself:
|
|
96
|
+
[docs/benchmark.md](docs/benchmark.md).
|
|
103
97
|
|
|
104
98
|
## What it does
|
|
105
99
|
|
|
@@ -109,245 +103,34 @@ reproduction; everything needed to rerun it ships in this repo.
|
|
|
109
103
|
[`src/patterns.js`](src/patterns.js).
|
|
110
104
|
- Sees through two transcript-specific disguises: a credential dumped only
|
|
111
105
|
as base64 on a line, or split across two adjacent streaming records, is
|
|
112
|
-
decoded/rejoined and rescanned
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
PlanetScale and MongoDB Atlas Service Account credentials. See
|
|
120
|
-
[`src/pairing.js`](src/pairing.js).
|
|
121
|
-
- Decodes a JWT's own `exp` claim locally (no network call, since the
|
|
122
|
-
claim is inside the signed payload) and reports "valid until" or
|
|
123
|
-
"expired" instead of just "last seen." See
|
|
124
|
-
[`src/jwtExpiry.js`](src/jwtExpiry.js).
|
|
106
|
+
decoded/rejoined and rescanned. See [`src/decode.js`](src/decode.js).
|
|
107
|
+
- Pairs an AWS secret access key with a nearby confirmed access key id
|
|
108
|
+
(also PlanetScale and MongoDB Atlas Service Account credentials) and
|
|
109
|
+
reports both at high confidence; ambiguous pairings are reported as
|
|
110
|
+
nothing rather than a guess. See [`src/pairing.js`](src/pairing.js).
|
|
111
|
+
- Decodes a JWT's own `exp` claim locally and reports "valid until" or
|
|
112
|
+
"expired" instead of just "last seen."
|
|
125
113
|
- **`--verify`** (opt-in, makes a real network call): asks a credential's
|
|
126
|
-
own vendor whether it still authenticates
|
|
127
|
-
|
|
128
|
-
[Verifying credentials are still live](#verifying-credentials-are-still-live)
|
|
129
|
-
below.
|
|
130
|
-
- With `--include-noisy`, filters broad generic-secret rules by how
|
|
131
|
-
machine-random the matched value looks; never applied to the default
|
|
132
|
-
rules. See [`src/rarity.js`](src/rarity.js).
|
|
114
|
+
own vendor whether it still authenticates. 35 vendors today, off by
|
|
115
|
+
default. See [docs/architecture.md](docs/architecture.md#verifying-credentials-are-still-live).
|
|
133
116
|
- Redacts everything in its own output, including `--json`: you get a
|
|
134
117
|
shape and a first/last-4 preview, never the real value.
|
|
135
|
-
- `--sarif` emits SARIF 2.1.0 for GitHub code scanning
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
instead of a typed passphrase. See [Sealing](#sealing-what-it-finds).
|
|
118
|
+
- `--sarif` emits SARIF 2.1.0 for GitHub code scanning.
|
|
119
|
+
- `--seal --keychain` encrypts every transcript with a finding into a
|
|
120
|
+
local vault. See [docs/architecture.md](docs/architecture.md#sealing-what-it-finds).
|
|
139
121
|
- Tells you how many **distinct** secrets it found versus how many times
|
|
140
122
|
one got echoed back across tool calls, so the headline number reflects
|
|
141
123
|
real exposure, not repetition.
|
|
142
|
-
- Flags likely placeholder/example matches separately from real findings,
|
|
143
|
-
rather than hiding them or inflating the count.
|
|
144
124
|
- Also scans agent **config files** and checks for **planted persistence**
|
|
145
|
-
(hooks, droppers, invisible Unicode)
|
|
125
|
+
(hooks, droppers, invisible Unicode) — a different, better-documented
|
|
126
|
+
leak surface. See [docs/architecture.md](docs/architecture.md#beyond-transcripts-configs-and-planted-persistence).
|
|
146
127
|
- Attaches a **rotation runbook** to every finding, plus a local
|
|
147
|
-
acknowledgement ledger. See [
|
|
148
|
-
- `--project <dir>` scans a repository checkout instead of the machine,
|
|
149
|
-
CI and pre-commit. See [
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- **`residoo mcp`**: query findings and manage rotation from inside Claude
|
|
154
|
-
Code itself, over a hand-rolled MCP server. See
|
|
155
|
-
[MCP: query findings from inside Claude Code](#mcp-query-findings-from-inside-claude-code)
|
|
156
|
-
below.
|
|
157
|
-
- **`residoo cred`**: store a live credential in the OS keychain and run
|
|
158
|
-
one allow-listed command with it injected as an environment variable,
|
|
159
|
-
never seen by the caller. See
|
|
160
|
-
[Cred: run commands with injected credentials](#cred-run-commands-with-injected-credentials)
|
|
161
|
-
below.
|
|
162
|
-
|
|
163
|
-
## Beyond transcripts: configs and planted persistence
|
|
164
|
-
|
|
165
|
-
Transcripts leak what your agent *saw*. Config files leak what it was
|
|
166
|
-
*configured with*, and that's the better-measured problem: GitGuardian
|
|
167
|
-
counted 24,008 secrets inside MCP config files on public GitHub (2,117
|
|
168
|
-
still valid), and Lakera found live credentials inside
|
|
169
|
-
`.claude/settings.local.json` shipped in ~30 published npm packages. So
|
|
170
|
-
`residoo scan` also covers the home-level config files of Claude Code,
|
|
171
|
-
Claude Desktop, Cursor, Gemini CLI, Codex, and Kiro, plus project-level
|
|
172
|
-
Claude Code configs (`.mcp.json`, `.claude/settings*.json`) resolved from
|
|
173
|
-
project roots the agent itself recorded, never by guessing directories.
|
|
174
|
-
|
|
175
|
-
Those same files are where 2026's supply-chain campaigns (Mini Shai-Hulud,
|
|
176
|
-
Miasma, the keyv/ChainDrop wave, TrapDoor) planted hooks, dropper scripts,
|
|
177
|
-
and zero-width-Unicode prompt injection. Every scan now also runs
|
|
178
|
-
**integrity checks** over those exact locations:
|
|
179
|
-
|
|
180
|
-
- Every auto-executing hook is listed; only a published campaign IOC or
|
|
181
|
-
campaign-shaped behavior (piping a download into a shell, base64-decode-
|
|
182
|
-
then-execute) escalates to a warning.
|
|
183
|
-
- Loose scripts in `.claude/` and known planted filenames are flagged by
|
|
184
|
-
name.
|
|
185
|
-
- `CLAUDE.md`, `.cursorrules`, and `.cursor/rules/*` are checked for
|
|
186
|
-
zero-width Unicode.
|
|
187
|
-
- `.vscode/tasks.json` is parsed for folder-open auto-run tasks.
|
|
188
|
-
|
|
189
|
-
Read-only like everything else. `--no-integrity` skips it entirely. A
|
|
190
|
-
config that can't be read is reported as unverified, never silently
|
|
191
|
-
counted clean.
|
|
192
|
-
|
|
193
|
-
## How it works
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
YOUR MACHINE · no network calls
|
|
197
|
-
┌───────────────────────────────────────────────────────────────┐
|
|
198
|
-
│ │
|
|
199
|
-
│ 42 transcript sources agent config files │
|
|
200
|
-
│ ~/.claude, Cursor, Codex… settings · MCP · memory │
|
|
201
|
-
│ (--project <dir>: a repo checkout instead of the machine) │
|
|
202
|
-
│ │ │ │
|
|
203
|
-
│ ├──────────────┬───────────────┤ │
|
|
204
|
-
│ ▼ │ ▼ │
|
|
205
|
-
│ stream + match │ integrity checks │
|
|
206
|
-
│ 50 verified rules │ hooks · droppers · │
|
|
207
|
-
│ │ │ zero-width unicode │
|
|
208
|
-
│ ▼ ▼ │ │
|
|
209
|
-
│ redacted report (first/last 4 chars only) ◀────────────┤
|
|
210
|
-
│ │ │
|
|
211
|
-
│ ├─▶ rotation hints per finding · explain / ack │
|
|
212
|
-
│ │ ledger: ~/.residoo/rotations.json │
|
|
213
|
-
│ │ │
|
|
214
|
-
│ ▼ --seal (only if you ask) │
|
|
215
|
-
│ AES-256-GCM vault · scrypt key · encrypted manifest │
|
|
216
|
-
│ │ │ │
|
|
217
|
-
│ ▼ unseal --restore ▼ --upload-cloudroam
|
|
218
|
-
│ SHA-256 verified copy ciphertext only ┄┄┄┄┄┄┄┄┄▶
|
|
219
|
-
│ │
|
|
220
|
-
└───────────────────────────────────────────────────────────────┘
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
The `--seal` and `--upload-cloudroam` legs never run unless you pass their
|
|
224
|
-
flag. Nothing in the diagram ever modifies or deletes an existing file. The
|
|
225
|
-
one exception, stated in the open: `residoo ack` writes residoo's own
|
|
226
|
-
rotation ledger at `~/.residoo/rotations.json` (atomic, redacted, never a
|
|
227
|
-
user file).
|
|
228
|
-
|
|
229
|
-
## Sealing what it finds
|
|
230
|
-
|
|
231
|
-
Finding a leaked key raises the obvious next question: *now what?*
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
residoo scan --seal
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Every transcript that carried a finding is encrypted into a local vault
|
|
238
|
-
directory: AES-256-GCM, key derived from your passphrase with scrypt,
|
|
239
|
-
streamed so an 800MB transcript never touches memory whole. The vault's own
|
|
240
|
-
manifest is encrypted too, so it doesn't advertise what's inside even by
|
|
241
|
-
name. **Originals are never touched.** Once you've verified a restore works
|
|
242
|
-
(`residoo unseal <vault> --restore 0001.sealed --out /tmp/check`, checked
|
|
243
|
-
byte-identical via a recorded SHA-256), deleting the plaintext is your
|
|
244
|
-
decision, made by you.
|
|
245
|
-
|
|
246
|
-
Optionally, `--upload-cloudroam` (with `CLOUDROAM_API_KEY`, `--connector`,
|
|
247
|
-
`--bucket`) copies the sealed vault to [CloudRoam](https://cloudroam.io) for
|
|
248
|
-
durable, cross-cloud storage.
|
|
249
|
-
|
|
250
|
-
> [!IMPORTANT]
|
|
251
|
-
> `--upload-cloudroam` is the *only* feature in residoo that touches the
|
|
252
|
-
> network to send your data anywhere. It never runs unless you pass the
|
|
253
|
-
> flag, and only ciphertext is transmitted: the vault is sealed before any
|
|
254
|
-
> upload code executes.
|
|
255
|
-
|
|
256
|
-
## Verifying credentials are still live
|
|
257
|
-
|
|
258
|
-
`--verify` asks a credential's own vendor whether it still authenticates,
|
|
259
|
-
using the exact value found in your transcript. Off by default, one real
|
|
260
|
-
network call per distinct credential.
|
|
261
|
-
|
|
262
|
-
Three vendors need a paired id+secret: **AWS** (via `sts:get-caller-identity`,
|
|
263
|
-
shelling out to your own `aws` CLI rather than reimplementing request
|
|
264
|
-
signing), **PlanetScale**, and **MongoDB Atlas** (Service Account
|
|
265
|
-
credentials only, the legacy Public/Private Key pair has no distinguishing
|
|
266
|
-
prefix and isn't detected at all). The other 32 are a single credential
|
|
267
|
-
each, one direct API call:
|
|
268
|
-
|
|
269
|
-
Slack · OpenAI · Anthropic · GitHub · Hugging Face · Replicate ·
|
|
270
|
-
DigitalOcean · Pinecone · SendGrid · Groq · xAI · OpenRouter · Stripe · npm ·
|
|
271
|
-
Notion · GitLab · Supabase · ElevenLabs · CircleCI · Airtable · Cloudflare ·
|
|
272
|
-
Heroku · Netlify · Linear · Telegram · Discord webhooks · Vercel · Cerebras ·
|
|
273
|
-
Render · Neon · PostHog · Fly.io
|
|
274
|
-
|
|
275
|
-
Every vendor clears the same two-stage bar before being wired up:
|
|
276
|
-
independent research against that vendor's own current docs, then a
|
|
277
|
-
separate, adversarial pass that tries to refute the first before it's
|
|
278
|
-
trusted. A real, sourced reason (no free endpoint, needs context the
|
|
279
|
-
credential doesn't carry, or a format not confirmed specifically enough to
|
|
280
|
-
detect safely) is why some detected credential types aren't wired to
|
|
281
|
-
`--verify` at all, not an oversight (Fly.io's `fm1a_`/`fm1r_`/`fm2_`
|
|
282
|
-
"macaroon" tokens are the clearest example: real-machine testing produced a
|
|
283
|
-
measured false-positive rate, so that family is detected nowhere in
|
|
284
|
-
residoo). A verified-active credential is escalated to "rotate
|
|
285
|
-
immediately"; a verified-invalid one is reported already dead, no action
|
|
286
|
-
needed. See [`src/verify.js`](src/verify.js).
|
|
287
|
-
|
|
288
|
-
## Rotation: from found to closed
|
|
289
|
-
|
|
290
|
-
Detection without rotation is theater: 64% of secrets leaked publicly in
|
|
291
|
-
2022 were still valid years later, 88% of re-verified leaked AWS keys still
|
|
292
|
-
authenticated, and the median time to remediate a GitHub-leaked secret is
|
|
293
|
-
94 days. Every finding in a residoo report comes with the way out:
|
|
294
|
-
|
|
295
|
-
- **A rotation hint per finding**, from a guidance map covering all 50
|
|
296
|
-
detection rules. Where shown, a rotation URL was fetched and confirmed to
|
|
297
|
-
document revoking that exact credential type.
|
|
298
|
-
- **`residoo explain <rule-id>`** prints the full runbook: where to revoke,
|
|
299
|
-
the steps, what revocation does. `residoo explain --list` shows the whole
|
|
300
|
-
catalogue.
|
|
301
|
-
- **`residoo ack <fingerprint>`** records that you rotated a finding.
|
|
302
|
-
**`residoo dismiss <fingerprint>`** records that it was never a real
|
|
303
|
-
secret. Both live in `~/.residoo/rotations.json`, residoo's own ledger,
|
|
304
|
-
written atomically, redacted through the same pipeline as previews.
|
|
305
|
-
- **"Recommended actions" leads the report**: how many *distinct* values
|
|
306
|
-
still need a decision, versus how many are already resolved (acked,
|
|
307
|
-
dismissed, or `--verify`-confirmed dead).
|
|
308
|
-
- **The rotation list groups by credential type**, so the URL prints once
|
|
309
|
-
per type. Each value's own line shows its redacted preview, file, and
|
|
310
|
-
when it was last seen.
|
|
311
|
-
- **Order matters, and the report says so.** The ChainDrop campaign (Aug
|
|
312
|
-
2026) shipped a token monitor that fires an attacker payload the moment a
|
|
313
|
-
stolen GitHub token is revoked. When a scan finds both integrity warnings
|
|
314
|
-
and leaked credentials, the report tells you to remove the planted
|
|
315
|
-
persistence first, rotate second.
|
|
316
|
-
|
|
317
|
-
Acks and dismissals change what the report *says*, never what CI *does*:
|
|
318
|
-
`--fail-on-find` fails on every finding, resolved or not, unless you pass
|
|
319
|
-
`--allow-acked` (integrity warnings always fail either way).
|
|
320
|
-
|
|
321
|
-
## CI and pre-commit
|
|
322
|
-
|
|
323
|
-
`residoo scan --project <dir>` scans a repository checkout instead of the
|
|
324
|
-
machine it runs on: committed transcripts, agent configs, root `.env`
|
|
325
|
-
files, plus integrity checks anchored at that directory. It never touches
|
|
326
|
-
the machine's home-level sources, so a clean CI run means the checkout is
|
|
327
|
-
clean and claims nothing about anyone's laptop.
|
|
328
|
-
|
|
329
|
-
As a GitHub Action (this repo doubles as a composite action):
|
|
330
|
-
|
|
331
|
-
```yaml
|
|
332
|
-
steps:
|
|
333
|
-
- uses: actions/checkout@v4
|
|
334
|
-
- uses: dandovdub/residoo@v0.8.0
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
As a pre-commit hook:
|
|
338
|
-
|
|
339
|
-
```yaml
|
|
340
|
-
repos:
|
|
341
|
-
- repo: https://github.com/dandovdub/residoo
|
|
342
|
-
rev: v0.8.0
|
|
343
|
-
hooks:
|
|
344
|
-
- id: residoo
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
Or with no integration at all:
|
|
348
|
-
`npm install -g residoo && residoo scan --project . --fail-on-find`.
|
|
349
|
-
Exit codes and exactly what project mode does and doesn't see are in
|
|
350
|
-
[docs/ci.md](docs/ci.md).
|
|
128
|
+
acknowledgement ledger. See [docs/architecture.md](docs/architecture.md#rotation-from-found-to-closed).
|
|
129
|
+
- `--project <dir>` scans a repository checkout instead of the machine,
|
|
130
|
+
for CI and pre-commit. See [docs/ci.md](docs/ci.md).
|
|
131
|
+
- `residoo watch` / `residoo mcp` / `residoo cred` / `residoo guard`:
|
|
132
|
+
continuous scanning, conversational queries, credential injection
|
|
133
|
+
without pasting, and pre-read blocking. See [docs/features.md](docs/features.md).
|
|
351
134
|
|
|
352
135
|
## What it does not do
|
|
353
136
|
|
|
@@ -364,6 +147,13 @@ Exit codes and exactly what project mode does and doesn't see are in
|
|
|
364
147
|
just encrypted a copy of.
|
|
365
148
|
- **No telemetry, no analytics, no update-check ping.**
|
|
366
149
|
|
|
150
|
+
Shape-based detection also can't tell a real secret from a realistic-
|
|
151
|
+
looking example in a fetched web page. Three suppression layers narrow the
|
|
152
|
+
gap, none catches every case, and all are re-includable with
|
|
153
|
+
`--include-suppressed`. Treat every finding as a lead to check, not a
|
|
154
|
+
certainty — true of every tool in this category, including the well-
|
|
155
|
+
established ones.
|
|
156
|
+
|
|
367
157
|
## Install
|
|
368
158
|
|
|
369
159
|
```bash
|
|
@@ -385,9 +175,10 @@ brew install residoo
|
|
|
385
175
|
The Homebrew formula installs the exact tarball published to npm (sha256
|
|
386
176
|
verified): same bits, not a second build.
|
|
387
177
|
|
|
388
|
-
Requires Node.js 18+ (22.5+ for the SQLite-backed sources listed
|
|
389
|
-
residoo still runs fine without it).
|
|
390
|
-
`package.json` rather than take that on
|
|
178
|
+
Requires Node.js 18+ (22.5+ for the SQLite-backed sources listed in
|
|
179
|
+
[docs/sources.md](docs/sources.md); residoo still runs fine without it).
|
|
180
|
+
Zero runtime dependencies: check `package.json` rather than take that on
|
|
181
|
+
faith.
|
|
391
182
|
|
|
392
183
|
## Usage
|
|
393
184
|
|
|
@@ -406,7 +197,7 @@ residoo scan [options]
|
|
|
406
197
|
--no-integrity skip the integrity checks
|
|
407
198
|
--no-color disable ANSI colour
|
|
408
199
|
--verify ask each credential's own vendor if it still authenticates
|
|
409
|
-
(real network call; see
|
|
200
|
+
(real network call; see docs/architecture.md)
|
|
410
201
|
|
|
411
202
|
--seal encrypt every transcript with findings into a local vault
|
|
412
203
|
--vault-dir <dir> vault location (default ./residoo-vault-<stamp>)
|
|
@@ -420,267 +211,18 @@ residoo ack <fingerprint> [--note <text>] mark one finding rotated
|
|
|
420
211
|
residoo unseal <vault-dir> list a vault's contents
|
|
421
212
|
residoo unseal <vault-dir> --restore <n> --out <p> restore one file, hash-verified
|
|
422
213
|
|
|
423
|
-
residoo watch
|
|
424
|
-
|
|
425
|
-
--interval <seconds> how often to check for new content (default 5, minimum 1)
|
|
426
|
-
--json NDJSON events on stdout, one line per finding/re-exposure
|
|
427
|
-
--verify same opt-in vendor check as scan --verify, applied to
|
|
428
|
-
each newly found credential once
|
|
429
|
-
--include-noisy, --include-suppressed, --no-color same meaning as scan
|
|
214
|
+
residoo watch / mcp / cred / guard see docs/features.md
|
|
430
215
|
```
|
|
431
216
|
|
|
432
217
|
The vault passphrase comes from `RESIDOO_PASSPHRASE` or a hidden interactive
|
|
433
218
|
prompt. There is no recovery if you lose it, so pick one you keep.
|
|
434
219
|
|
|
435
|
-
## Watch: continuous scanning
|
|
436
|
-
|
|
437
|
-
`residoo scan` is a snapshot. `residoo watch` is the same engine run
|
|
438
|
-
continuously: it polls every source `scan` already covers, and the moment a
|
|
439
|
-
new secret lands in a transcript, prints an alert with the redacted value,
|
|
440
|
-
the rule, the file, and the same rotation runbook a scan finding carries,
|
|
441
|
-
instead of waiting for you to remember to run `scan` again.
|
|
442
|
-
|
|
443
|
-
```
|
|
444
|
-
$ residoo watch
|
|
445
|
-
watching 43 sources, 118 files (61 tailed, 57 rescanned on change)
|
|
446
|
-
polling every 5s; fs.watch is not used, every alert comes from polling
|
|
447
|
-
|
|
448
|
-
2026-09-03 14:02:11 [high] AWS Access Key ID AKIA****ABCD
|
|
449
|
-
claude-code · session-9f2c.jsonl:214 · rf1-8a3e91 Rotate: https://.../access_keys
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
It is watch-from-**now**: run `residoo scan` first for anything already on
|
|
453
|
-
disk, since a fresh `residoo watch` baselines whatever it finds on its first
|
|
454
|
-
sweep silently and only alerts on content written after it starts. A finding
|
|
455
|
-
already acknowledged or dismissed (`residoo ack` / `residoo dismiss`) stays
|
|
456
|
-
suppressed, checked against the same `~/.residoo/rotations.json` ledger, and
|
|
457
|
-
a ledger change made mid-watch takes effect within one poll interval, no
|
|
458
|
-
restart needed. A findings-free sweep prints nothing at all, including to
|
|
459
|
-
its own watched Claude Code session, by design: no other tool in this
|
|
460
|
-
project's own benchmark ([`bench/`](bench/)) has a continuous mode at all,
|
|
461
|
-
verified directly against each one's own `--help` output rather than
|
|
462
|
-
assumed; see [docs/comparison.md](docs/comparison.md) for how the one
|
|
463
|
-
adjacent thing, GitGuardian's `ggshield` AI hook, works differently.
|
|
464
|
-
|
|
465
|
-
## MCP: query findings from inside Claude Code
|
|
466
|
-
|
|
467
|
-
`residoo mcp` runs residoo as an MCP server over stdio, so Claude Code can
|
|
468
|
-
query findings and manage the rotation ledger conversationally instead of
|
|
469
|
-
you running the CLI in a terminal:
|
|
470
|
-
|
|
471
|
-
```bash
|
|
472
|
-
claude mcp add residoo -- residoo mcp
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
or run `scripts/install-mcp.sh` (also in this repo), which installs residoo
|
|
476
|
-
itself first if it isn't already, then registers it and verifies the
|
|
477
|
-
connection.
|
|
478
|
-
|
|
479
|
-
or add it directly to `.mcp.json`:
|
|
480
|
-
|
|
481
|
-
```json
|
|
482
|
-
{
|
|
483
|
-
"mcpServers": {
|
|
484
|
-
"residoo": { "type": "stdio", "command": "residoo", "args": ["mcp"] }
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
Five read-only tools, mirroring the CLI exactly: `residoo_scan` (a fresh
|
|
490
|
-
scan, merged with rotation status), `residoo_check` (only what's new
|
|
491
|
-
since the last check in this conversation, backed by the same engine as
|
|
492
|
-
`watch`), `residoo_explain` (a rule's rotation runbook), and
|
|
493
|
-
`residoo_ack` / `residoo_dismiss` (append to the local ledger). Every
|
|
494
|
-
value returned is redacted the same way the CLI's own output is; nothing
|
|
495
|
-
here makes a network call or touches the transcript files themselves.
|
|
496
|
-
Like the rest of residoo, this is hand-rolled against the MCP spec
|
|
497
|
-
directly, not built on `@modelcontextprotocol/sdk`: zero runtime
|
|
498
|
-
dependencies stays true here too. A sixth, opt-in tool exists for
|
|
499
|
-
injected-credential execution, covered below.
|
|
500
|
-
|
|
501
|
-
A seventh tool, `residoo_verify_finding`, is genuinely different from the
|
|
502
|
-
other six: it asks a credential's own vendor, live, whether it's still
|
|
503
|
-
active (the same mechanism as `scan --verify`, scoped to exactly one
|
|
504
|
-
credential per call). This is the one MCP tool that makes a real network
|
|
505
|
-
call, so it does not exist at all unless you set
|
|
506
|
-
`RESIDOO_MCP_ALLOW_VERIFY=1` in the environment `residoo mcp` runs in — a
|
|
507
|
-
default install stays true to "zero network calls" without a caveat.
|
|
508
|
-
Once enabled, pass a `fingerprint` from a prior `residoo_scan`; you get
|
|
509
|
-
back `active`, `invalid`, or `unknown`, never the raw value. Paired
|
|
510
|
-
credentials (AWS access key + secret, PlanetScale, MongoDB Atlas) aren't
|
|
511
|
-
supported yet — use `residoo scan --verify` from a terminal for those.
|
|
512
|
-
|
|
513
|
-
## Cred: run commands with injected credentials
|
|
514
|
-
|
|
515
|
-
The usual way an AI coding agent ends up able to use a real credential is
|
|
516
|
-
you pasting it into the chat, which puts it in that conversation's
|
|
517
|
-
transcript forever, indistinguishable from any other leak `residoo scan`
|
|
518
|
-
finds. `residoo cred` is the alternative: store the credential once in
|
|
519
|
-
your OS keychain, then let Claude run one allow-listed command with it
|
|
520
|
-
injected as environment variables. Claude never sees the raw value,
|
|
521
|
-
before, during, or after, and it's never written into a script either.
|
|
522
|
-
|
|
523
|
-
```bash
|
|
524
|
-
residoo cred set aws-prod --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY
|
|
525
|
-
# hidden-typed input, once per --env flag; interactive TTY only, no
|
|
526
|
-
# scripted entry, since a live credential is more sensitive than a vault
|
|
527
|
-
# passphrase and should never be typeable into a script or env var.
|
|
528
|
-
|
|
529
|
-
RESIDOO_CRED_ALLOWED_COMMANDS="aws=/usr/local/bin/aws" residoo mcp
|
|
530
|
-
# now residoo_run_with_cred exists as an MCP tool; it does not exist at
|
|
531
|
-
# all (won't appear in the tool list) until this is set.
|
|
532
|
-
|
|
533
|
-
residoo cred run aws-prod -- aws s3 ls
|
|
534
|
-
# same operation from a terminal, for testing without an MCP client.
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
**Why this is safer than it looks, stated precisely, not just asserted:**
|
|
538
|
-
|
|
539
|
-
- `RESIDOO_CRED_ALLOWED_COMMANDS` (`name=/absolute/path,...`) is an
|
|
540
|
-
environment variable the operator sets outside any conversation, read
|
|
541
|
-
fresh on every single invocation. Empty or unset means **nothing may
|
|
542
|
-
run, by design**: this is the actual, sole security boundary.
|
|
543
|
-
- The `command` a caller (human or model) supplies is used **only as a
|
|
544
|
-
lookup key** into that map, never as a path, never resolved via
|
|
545
|
-
`PATH`. This was not always true: a first draft matched by binary
|
|
546
|
-
*name* alone (checked fresh every call, but only verifying the name the
|
|
547
|
-
caller claimed, not the binary that actually ran), and an adversarial
|
|
548
|
-
review found two concrete ways around that: a caller-supplied path
|
|
549
|
-
smuggled straight past the check, and a same-named malicious binary
|
|
550
|
-
planted earlier on the server process's own inherited `PATH`. Both are
|
|
551
|
-
closed structurally now: `command` cannot cause any file other than the
|
|
552
|
-
operator-pinned absolute path to execute, full stop.
|
|
553
|
-
- Arguments are always a structured list, never a shell string, so no
|
|
554
|
-
shell metacharacter ever gets interpreted.
|
|
555
|
-
- The executed command's own stdout/stderr content is **never returned**,
|
|
556
|
-
only exit status and line counts, because that output is itself a
|
|
557
|
-
channel the injected secret could leak through in ways no redaction
|
|
558
|
-
pass can guarantee to catch (an echoed variable, a stack trace).
|
|
559
|
-
- A hung command is killed after 30 seconds, not configurable by the
|
|
560
|
-
caller (letting a model choose its own timeout has no legitimate use
|
|
561
|
-
and only helps an attacker keep a process alive longer).
|
|
562
|
-
- One line goes to `residoo mcp`'s own **stderr** per credential use
|
|
563
|
-
(timestamp, credential name, command, exit code, never the value or
|
|
564
|
-
the arguments). This is **not durable by default**: redirect the MCP
|
|
565
|
-
server's stderr at launch (`residoo mcp 2>> ~/.residoo-cred-audit.log`,
|
|
566
|
-
or your MCP client's equivalent) if you want a persistent trail.
|
|
567
|
-
|
|
568
|
-
**Only ever allow-list narrow, single-purpose CLIs**, never a tool that
|
|
569
|
-
can itself run arbitrary third-party code as part of normal operation
|
|
570
|
-
(`npm`, `npx`, `pip`, `make`, `cargo`, any build tool). Watch out even for
|
|
571
|
-
a seemingly narrow tool with its own plugin system: an allow-listed `gh`
|
|
572
|
-
still receives the injected credential as an inherited environment
|
|
573
|
-
variable in whatever `gh extension exec` or `gh alias` runs, which is
|
|
574
|
-
untrusted the moment it's a third-party extension. This is a residual
|
|
575
|
-
risk allow-listing alone doesn't remove, so narrow the tools you allow-list
|
|
576
|
-
accordingly, and prefer credentials scoped as tightly as the vendor
|
|
577
|
-
allows.
|
|
578
|
-
|
|
579
|
-
Storage is macOS (`security`) or Linux (`secret-tool`) only, matching
|
|
580
|
-
`--seal --keychain`'s own existing platform support; Windows is refused
|
|
581
|
-
with a clear message rather than half-built. There is no `residoo cred
|
|
582
|
-
list` in v1: you need to already know the name you set.
|
|
583
|
-
|
|
584
|
-
## Guard: block a sensitive read before it happens
|
|
585
|
-
|
|
586
|
-
Everything above finds a leak after it's already written to disk. `residoo
|
|
587
|
-
guard` is the one piece of residoo that tries to stop one from happening in
|
|
588
|
-
the first place — a Claude Code `PreToolUse` hook that blocks an obviously-
|
|
589
|
-
sensitive file read (`.env`, `id_rsa`, `.aws/credentials`, and similar)
|
|
590
|
-
before the command runs at all.
|
|
591
|
-
|
|
592
|
-
```json
|
|
593
|
-
{
|
|
594
|
-
"hooks": {
|
|
595
|
-
"PreToolUse": [
|
|
596
|
-
{ "matcher": "Bash|Read", "hooks": [{ "type": "command", "command": "residoo guard" }] }
|
|
597
|
-
]
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
in `.claude/settings.json`. It reads one hook payload from stdin and writes
|
|
602
|
-
a deny decision to stdout only when the proposed command or file path
|
|
603
|
-
matches; anything it doesn't recognize falls through untouched, with zero
|
|
604
|
-
output, exit 0.
|
|
605
|
-
|
|
606
|
-
This is narrower than it might sound, and the gap is worth stating
|
|
607
|
-
plainly rather than implying more than it does: Claude Code's hooks API
|
|
608
|
-
lets a `PreToolUse` hook see the proposed tool INPUT (a Bash command
|
|
609
|
-
string, a Read file path) before it runs, but there is no documented
|
|
610
|
-
mechanism for a hook to see or redact a tool's OUTPUT — by the time a
|
|
611
|
-
`PostToolUse` hook fires, that output is already committed to the
|
|
612
|
-
transcript. So this can only block on the shape of the request, never
|
|
613
|
-
clean up what a command already printed. It will not catch a secret typed
|
|
614
|
-
directly into a prompt, or one arriving in the output of an otherwise
|
|
615
|
-
unremarkable command (`curl`, a build log). `residoo scan` / `watch` /
|
|
616
|
-
`mcp` remain the actual safety net; this is a best-effort tripwire on top
|
|
617
|
-
of them, not a replacement.
|
|
618
|
-
|
|
619
220
|
## Sources supported today
|
|
620
221
|
|
|
621
|
-
43 sources
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
**Real-install-verified**: run against an actual, populated installation
|
|
626
|
-
and confirmed to find real content: **Claude Code**
|
|
627
|
-
(`~/.claude/projects/**/*.jsonl`) and **agent config files** for its
|
|
628
|
-
Claude-family paths.
|
|
629
|
-
|
|
630
|
-
**Multi-source-corroborated-but-unverified**: backed by 2+ independent,
|
|
631
|
-
credible sources but not checked against a real install on any machine this
|
|
632
|
-
project was built on. Still built to fail loudly rather than silently
|
|
633
|
-
report "all clear":
|
|
634
|
-
|
|
635
|
-
Cursor, Codex CLI, OpenCode, Aider, Cline, Roo Code, Kilo Code, Windsurf,
|
|
636
|
-
PearAI, Trae, Void, Gemini CLI, Qwen Code, Continue, Open Interpreter,
|
|
637
|
-
Goose, GitHub Copilot Chat/CLI, `llm`, Codebuff, Mentat, Hermes, OpenClaw,
|
|
638
|
-
Warp, Crush, Grok Build, Kiro CLI/IDE, Zed, JetBrains Junie/AI Assistant,
|
|
639
|
-
Sourcegraph Cody, Amazon Q Developer, Qodo Gen, OpenHands, Factory Droid
|
|
640
|
-
CLI, Devin CLI, Pi, Google Antigravity, Kimi Code, and `fx`.
|
|
641
|
-
|
|
642
|
-
A few are SQLite-backed (Cursor, Crush, Cody, Devin CLI, Hermes, Kiro CLI,
|
|
643
|
-
`llm`, Trae, Void, Warp, Zed) and need Node.js 22.5+ for the built-in
|
|
644
|
-
`node:sqlite` module. On an older Node, each reports as detected-but-not-
|
|
645
|
-
scanned rather than silently dropping.
|
|
646
|
-
|
|
647
|
-
**Investigated and deliberately not included:** Plandex (client-server,
|
|
648
|
-
nothing local to scan), CodeGPT and Augment Code (account/cloud-based, no
|
|
649
|
-
local transcript file), Replit Agent (cloud-only). Tabby, Tabnine,
|
|
650
|
-
Zencoder, Tongyi Lingma, and Berd didn't clear the 2-independent-source bar
|
|
651
|
-
in the time available. A verified adapter for any of these is a welcome PR.
|
|
652
|
-
|
|
653
|
-
See [`src/sources/index.js`](src/sources/index.js) for the full list, and
|
|
654
|
-
each source file's own header for exactly what was and wasn't checked.
|
|
655
|
-
|
|
656
|
-
## Adding a source
|
|
657
|
-
|
|
658
|
-
A source is a small object with four methods: `id()`, `label()`,
|
|
659
|
-
`available()`, `files()`, `readLines(file)`.
|
|
660
|
-
[`src/sources/claude-code.js`](src/sources/claude-code.js) is the reference
|
|
661
|
-
implementation: copy it, point it at your tool's real local storage path,
|
|
662
|
-
open a PR. Two contracts worth getting right:
|
|
663
|
-
|
|
664
|
-
- **`files()`** yields `{ file, mtimeMs, sizeBytes, broken }`. Set
|
|
665
|
-
`broken: true` for an entry that looked scannable but wasn't (a dangling
|
|
666
|
-
symlink); don't just skip past it silently.
|
|
667
|
-
- **`readLines(file)`** is `async`, returning `{ lines, status, bytesRead }`
|
|
668
|
-
with `status` one of `"complete"`, `"partial"`, `"too-large"`, `"failed"`.
|
|
669
|
-
Whatever's in `lines` for a non-`"complete"` status still gets scanned.
|
|
670
|
-
|
|
671
|
-
Please verify the path actually exists and holds real content before
|
|
672
|
-
submitting.
|
|
673
|
-
|
|
674
|
-
## A known limitation, stated plainly
|
|
675
|
-
|
|
676
|
-
Shape-based detection can't tell a real secret from a realistic-looking
|
|
677
|
-
example in a fetched web page or documentation your agent read back to
|
|
678
|
-
you. Three suppression layers narrow the gap (known vendor-documented
|
|
679
|
-
example values, placeholder bodies built from one repeated character, and
|
|
680
|
-
placeholder-looking surrounding context), but none catches every case, and
|
|
681
|
-
all are re-includable with `--include-suppressed`. Treat every finding as a
|
|
682
|
-
lead to check, not a certainty. The same is true of every tool in this
|
|
683
|
-
category, including the well-established ones.
|
|
222
|
+
43 sources, real-install-verified for Claude Code and its config family,
|
|
223
|
+
multi-source-corroborated for the rest (Cursor, Codex CLI, Cline, Windsurf,
|
|
224
|
+
Gemini CLI, Copilot, and 30+ more). Full list, what "corroborated" means,
|
|
225
|
+
and how to add one: [docs/sources.md](docs/sources.md).
|
|
684
226
|
|
|
685
227
|
## License
|
|
686
228
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CloudRoam (https://cloudroam.io)",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node tests/smoke.js"
|
|
23
|
+
"test": "node tests/smoke.js",
|
|
24
|
+
"fuzz": "node tests/fuzz.js"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"bin",
|
|
@@ -41,5 +42,8 @@
|
|
|
41
42
|
"privacy",
|
|
42
43
|
"cli",
|
|
43
44
|
"encryption"
|
|
44
|
-
]
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"fast-check": "4.9.0"
|
|
48
|
+
}
|
|
45
49
|
}
|
package/src/guard.js
CHANGED
|
@@ -39,18 +39,45 @@ const BOUNDARY = "(?:$|[\\s'\"`;|&)<>])";
|
|
|
39
39
|
// Left boundary: start of string, a path separator (mid-path, e.g.
|
|
40
40
|
// "/foo/.env"), OR whitespace/a shell metacharacter (the path is one
|
|
41
41
|
// argument in a longer command, e.g. "cat .env && echo done" -- ".env" is
|
|
42
|
-
// preceded by a space, not a separator).
|
|
42
|
+
// preceded by a space, not a separator). Deliberately NOT a hyphen: an
|
|
43
|
+
// earlier version added one (to catch "gcp-service-account-prod.json",
|
|
44
|
+
// see the dedicated pattern below instead) and it broke the public.pem/
|
|
45
|
+
// public.key exclusions below -- a negative lookahead only guards its own
|
|
46
|
+
// anchor position, and a hyphen boundary let the regex engine start
|
|
47
|
+
// matching again from a LATER position inside the same filename (e.g.
|
|
48
|
+
// right after "public-" in "public-key.pem"), silently walking around the
|
|
49
|
+
// exclusion. Kept narrow and per-pattern instead of widening this shared
|
|
50
|
+
// primitive for one case.
|
|
43
51
|
const SEP = "(?:^|[\\s'\"`;|&(<>\\\\/])";
|
|
44
52
|
const pat = (body) => new RegExp(SEP + body + BOUNDARY, "i");
|
|
45
53
|
|
|
54
|
+
// Suffixes that make a .env-shaped path a committed, secret-free template
|
|
55
|
+
// rather than the real thing: never planted with live credentials by
|
|
56
|
+
// convention, and reading one is completely routine (checking which vars
|
|
57
|
+
// a project needs). Found by testing this guard against realistic dev
|
|
58
|
+
// commands, not assumed: cat .env.example was blocked before this existed.
|
|
59
|
+
const ENV_SAFE_SUFFIX = "example|sample|template|dist|default|schema";
|
|
60
|
+
|
|
46
61
|
const SENSITIVE_PATH_PATTERNS = [
|
|
47
|
-
// dotenv files, including staged/numbered variants (.env.local, .env.1)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
// dotenv files, including staged/numbered variants (.env.local, .env.1),
|
|
63
|
+
// but not a known-safe template suffix (see ENV_SAFE_SUFFIX above).
|
|
64
|
+
{ re: pat(`\\.env(?:\\.(?!(?:${ENV_SAFE_SUFFIX})(?:$|[\\s'"\`;|&)<>.]))[\\w.-]+)?`), label: "a .env file" },
|
|
65
|
+
// SSH private keys: the conventional default names. Deliberately NOT
|
|
66
|
+
// *.pub -- a public key is, by definition, meant to be shared (it's
|
|
67
|
+
// what you paste into GitHub's own SSH keys page); blocking its read
|
|
68
|
+
// protects nothing and was a real false positive found the same way.
|
|
69
|
+
{ re: pat("id_(?:rsa|dsa|ecdsa|ed25519)"), label: "an SSH private key" },
|
|
70
|
+
// The whole .ssh directory, EXCEPT its own *.pub files and known_hosts
|
|
71
|
+
// (host key fingerprints, not credentials -- reading it can't expose
|
|
72
|
+
// anything) -- same public-key/not-actually-sensitive principle as the
|
|
73
|
+
// id_/*.pem/*.key exclusions above, applied to a directory match.
|
|
74
|
+
{ re: new RegExp(SEP + "\\.ssh[\\\\/](?!(?:[\\w.-]+\\.pub|known_hosts(?:\\.old)?)(?:$|[\\s'\"`;|&)<>]))", "i"), label: "the SSH directory" },
|
|
75
|
+
// *.pem/*.key, except a filename that itself says "public": a real
|
|
76
|
+
// private key is never conventionally named that way, and "public.pem"/
|
|
77
|
+
// "public-key.pem" naming a non-sensitive cert is common enough that
|
|
78
|
+
// blocking it is pure noise, not protection.
|
|
79
|
+
{ re: pat("(?!public[-_.])[\\w.-]+\\.pem"), label: "a .pem key file" },
|
|
80
|
+
{ re: pat("(?!public[-_.])[\\w.-]+\\.key"), label: "a .key file" },
|
|
54
81
|
// cloud / vendor credential files with a fixed, well-known name
|
|
55
82
|
{ re: pat("\\.aws[\\\\/](?:credentials|config)"), label: "the AWS credentials file" },
|
|
56
83
|
{ re: pat("\\.netrc"), label: "the .netrc file" },
|
|
@@ -60,7 +87,14 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
60
87
|
{ re: pat("\\.kube[\\\\/]config"), label: "the kubeconfig file" },
|
|
61
88
|
{ re: pat("application_default_credentials\\.json"), label: "gcloud application-default credentials" },
|
|
62
89
|
{ re: pat("credentials\\.json"), label: "a credentials.json file" },
|
|
63
|
-
|
|
90
|
+
// No SEP prefix here, on purpose: a real, common naming convention
|
|
91
|
+
// prefixes this with a project/company name and a hyphen (e.g.
|
|
92
|
+
// "gcp-service-account-prod.json"), which the standard SEP boundary
|
|
93
|
+
// (start/whitespace/separator, deliberately not a hyphen -- see SEP's
|
|
94
|
+
// own comment) would miss entirely. "service[_-]?account" is specific
|
|
95
|
+
// enough as a token that not requiring a left boundary here is a safe,
|
|
96
|
+
// narrow exception rather than a reason to widen SEP itself.
|
|
97
|
+
{ re: new RegExp("service[_-]?account[\\w.-]*\\.json" + BOUNDARY, "i"), label: "a GCP service-account key file" },
|
|
64
98
|
{ re: pat("secrets?\\.(?:json|ya?ml)"), label: "a secrets file" },
|
|
65
99
|
];
|
|
66
100
|
|
package/src/patterns.js
CHANGED
|
@@ -145,11 +145,16 @@ const PATTERNS = [
|
|
|
145
145
|
// Confirmed via airtable.com/developers/web/api: pat + 14 alnum + "." + 64 hex.
|
|
146
146
|
{ id: "airtable_token", label: "Airtable personal access token", confidence: "high",
|
|
147
147
|
re: /\bpat[A-Za-z0-9]{14}\.[a-f0-9]{64}\b/g },
|
|
148
|
-
// Current Cloudflare
|
|
149
|
-
// developers.cloudflare.com
|
|
150
|
-
//
|
|
148
|
+
// Current Cloudflare credential formats only (cfat_/cfut_/cfk_, all three
|
|
149
|
+
// confirmed via developers.cloudflare.com/fundamentals/api/get-started/token-formats,
|
|
150
|
+
// which describes all three with the identical "<prefix>_[40 characters]
|
|
151
|
+
// [checksum]" shape). cfk_ (Global API Key, full account access) found
|
|
152
|
+
// missing by cross-checking agentsweep's own open issue tracker -- it had
|
|
153
|
+
// cfat_/cfut_ before this project did, cfk_ after. The legacy formats are
|
|
154
|
+
// bare unprefixed strings, left out for the same reason as CircleCI's
|
|
155
|
+
// legacy form.
|
|
151
156
|
{ id: "cloudflare_api_token", label: "Cloudflare API token", confidence: "high",
|
|
152
|
-
re: /\bcf[au]
|
|
157
|
+
re: /\bcf(?:[au]t|k)_[a-zA-Z0-9]{40}[a-f0-9]{8}\b/g },
|
|
153
158
|
// Current Heroku API key format only (HRKU-AA + 58 chars, confirmed via
|
|
154
159
|
// Heroku's own help docs). The legacy format is a bare UUID, left out:
|
|
155
160
|
// "any UUID-shaped string" is exactly the noisy, unspecific shape this
|