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 CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/residoo)](https://www.npmjs.com/package/residoo)
12
12
  [![CI](https://github.com/dandovdub/residoo/actions/workflows/ci.yml/badge.svg)](https://github.com/dandovdub/residoo/actions/workflows/ci.yml)
13
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/dandovdub/residoo/badge)](https://scorecard.dev/viewer/?uri=github.com/dandovdub/residoo)
13
14
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
15
  [![node >=18](https://img.shields.io/badge/node-%3E%3D18-339933)](package.json)
15
16
  [![runtime dependencies: 0](https://img.shields.io/badge/runtime_dependencies-0-brightgreen)](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.4.14:
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.1
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.1
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
- `residoo cred` stores a live, reusable credential in the OS keychain and
479
- runs one allow-listed command with it injected as environment variables:
480
- Claude never sees the raw value, before, during, or after:
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.1",
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
- if (start < seam && end > seam) straddles.push({ start, end, value: m[0] });
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;