residoo 0.7.1 → 0.7.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 +31 -6
- package/SECURITY.md +10 -1
- package/package.json +1 -1
- package/src/decode.js +28 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/residoo)
|
|
12
12
|
[](https://github.com/dandovdub/residoo/actions/workflows/ci.yml)
|
|
13
|
+
[](https://scorecard.dev/viewer/?uri=github.com/dandovdub/residoo)
|
|
13
14
|
[](LICENSE)
|
|
14
15
|
[](package.json)
|
|
15
16
|
[](package.json)
|
|
@@ -60,13 +61,33 @@ watching 43 sources, 118 files · polling every 5s
|
|
|
60
61
|
> trufflehog/betterleaks' verification postures, in
|
|
61
62
|
> [docs/comparison.md](docs/comparison.md).
|
|
62
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
|
+
|
|
63
84
|
## Benchmark: measured, not claimed
|
|
64
85
|
|
|
65
86
|
A reproducible benchmark against 8 real competing tools, on a synthetic-but-
|
|
66
87
|
pattern-true corpus (72 Claude Code sessions, 45 planted credentials, zero
|
|
67
88
|
real secrets), with live egress monitoring so "no network calls" is
|
|
68
89
|
observed, not just documented. Re-run against every meaningful release,
|
|
69
|
-
most recently v0.
|
|
90
|
+
most recently v0.7.2:
|
|
70
91
|
|
|
71
92
|
| | residoo | best of the rest |
|
|
72
93
|
|---|---|---|
|
|
@@ -310,7 +331,7 @@ As a GitHub Action (this repo doubles as a composite action):
|
|
|
310
331
|
```yaml
|
|
311
332
|
steps:
|
|
312
333
|
- uses: actions/checkout@v4
|
|
313
|
-
- uses: dandovdub/residoo@v0.7.
|
|
334
|
+
- uses: dandovdub/residoo@v0.7.2
|
|
314
335
|
```
|
|
315
336
|
|
|
316
337
|
As a pre-commit hook:
|
|
@@ -318,7 +339,7 @@ As a pre-commit hook:
|
|
|
318
339
|
```yaml
|
|
319
340
|
repos:
|
|
320
341
|
- repo: https://github.com/dandovdub/residoo
|
|
321
|
-
rev: v0.7.
|
|
342
|
+
rev: v0.7.2
|
|
322
343
|
hooks:
|
|
323
344
|
- id: residoo
|
|
324
345
|
```
|
|
@@ -475,9 +496,13 @@ injected-credential execution, covered below.
|
|
|
475
496
|
|
|
476
497
|
## Cred: run commands with injected credentials
|
|
477
498
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
499
|
+
The usual way an AI coding agent ends up able to use a real credential is
|
|
500
|
+
you pasting it into the chat, which puts it in that conversation's
|
|
501
|
+
transcript forever, indistinguishable from any other leak `residoo scan`
|
|
502
|
+
finds. `residoo cred` is the alternative: store the credential once in
|
|
503
|
+
your OS keychain, then let Claude run one allow-listed command with it
|
|
504
|
+
injected as environment variables. Claude never sees the raw value,
|
|
505
|
+
before, during, or after, and it's never written into a script either.
|
|
481
506
|
|
|
482
507
|
```bash
|
|
483
508
|
residoo cred set aws-prod --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY
|
package/SECURITY.md
CHANGED
|
@@ -83,7 +83,16 @@ the kind of thing worth impersonating.
|
|
|
83
83
|
built it; check the provenance badge on the npm page. One honest
|
|
84
84
|
exception, stated rather than hidden: the very first release (v0.1.0)
|
|
85
85
|
was a manual upload to claim the name, so provenance starts at the first
|
|
86
|
-
CI-published version after it.
|
|
86
|
+
CI-published version after it. Concretely, this is npm's own [Trusted
|
|
87
|
+
Publishing](https://docs.npmjs.com/trusted-publishers): no stored token
|
|
88
|
+
anywhere, GitHub mints a short-lived, workflow-scoped OIDC credential per
|
|
89
|
+
release, and the resulting provenance statement is signed and published
|
|
90
|
+
to the public [Sigstore transparency log](https://search.sigstore.dev/),
|
|
91
|
+
independently checkable by anyone, not just trusted on residoo's word.
|
|
92
|
+
In [SLSA](https://slsa.dev/) terms that's Build Level 2 (a hosted,
|
|
93
|
+
authenticated build platform generates non-forgeable provenance); Level
|
|
94
|
+
3 (fully hermetic, isolated builds) isn't implemented yet, stated
|
|
95
|
+
plainly rather than implied.
|
|
87
96
|
- The only PyPI package is **`residoo`**: a thin official launcher whose
|
|
88
97
|
entire job is running the npm CLI via `npx`. Its source lives in this
|
|
89
98
|
repository under `pypi/`. It exists partly so nobody else can hold the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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)",
|
package/src/decode.js
CHANGED
|
@@ -306,6 +306,25 @@ function findDecodedMatches(line, rules) {
|
|
|
306
306
|
const BOUNDARY_WINDOW = 300; // chars taken from each side of the seam
|
|
307
307
|
const BOUNDARY_MIN_CONTENT = 24; // shorter "longest string" is treated as non-content
|
|
308
308
|
|
|
309
|
+
// Minimum characters EACH side of the seam must contribute to a straddling
|
|
310
|
+
// match for it to be trusted as a genuine split, not coincidence. Found via
|
|
311
|
+
// this project's own benchmark stress-testing (2026-09-03), not hypothetical:
|
|
312
|
+
// a variable-length rule (bearer_header, {16,1000}) can sit ONE character
|
|
313
|
+
// short of its own minimum at the end of a line (a near-miss, not a complete
|
|
314
|
+
// match — the existing greedy-extension guard above only recognizes COMPLETE
|
|
315
|
+
// tail-alone matches, so it never sees this case), and if the very next
|
|
316
|
+
// line's content happens to start with even one or two more characters the
|
|
317
|
+
// pattern's class allows, the straddle pass stitches two entirely unrelated,
|
|
318
|
+
// benign lines into a fabricated value that exists in neither. Existing
|
|
319
|
+
// legitimate-split tests (tests/smoke.js) cut real secrets 9-11 characters
|
|
320
|
+
// from each end, comfortably clear of this floor; a genuine chunked-
|
|
321
|
+
// streaming boundary landing with less than this on one side, while
|
|
322
|
+
// possible, is far rarer than the coincidental-concatenation failure mode
|
|
323
|
+
// this exists to close, and the fragment still gets caught by the raw,
|
|
324
|
+
// single-line pass once enough of it lands on either side to satisfy the
|
|
325
|
+
// rule outright.
|
|
326
|
+
const BOUNDARY_MIN_CONTRIBUTION = 4;
|
|
327
|
+
|
|
309
328
|
/**
|
|
310
329
|
* Escape-aware list of JSON string-literal CONTENTS on a line. Field names
|
|
311
330
|
* are included (this walker does not distinguish keys from values); the
|
|
@@ -395,8 +414,15 @@ function findBoundaryMatches(contentA, contentB, rules) {
|
|
|
395
414
|
const start = m.index;
|
|
396
415
|
const end = m.index + m[0].length;
|
|
397
416
|
// Straddle-only: the match must cross the seam, else it lay wholly in
|
|
398
|
-
// one line and the single-line pass already reported it.
|
|
399
|
-
|
|
417
|
+
// one line and the single-line pass already reported it. Each side
|
|
418
|
+
// must also contribute a real fragment (BOUNDARY_MIN_CONTRIBUTION,
|
|
419
|
+
// see its own doc comment) -- otherwise this is a near-miss single-
|
|
420
|
+
// line match that unrelated adjacent content happened to push over a
|
|
421
|
+
// length-quantifier's minimum, not a genuine split.
|
|
422
|
+
if (start < seam && end > seam &&
|
|
423
|
+
(seam - start) >= BOUNDARY_MIN_CONTRIBUTION && (end - seam) >= BOUNDARY_MIN_CONTRIBUTION) {
|
|
424
|
+
straddles.push({ start, end, value: m[0] });
|
|
425
|
+
}
|
|
400
426
|
if (m.index === rule.re.lastIndex) rule.re.lastIndex++;
|
|
401
427
|
}
|
|
402
428
|
let flush = null;
|