residoo 0.8.3 → 0.8.5

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
@@ -97,7 +97,7 @@ while losing rows, then fixed in public against the classes it was losing
97
97
 
98
98
  ## What it does
99
99
 
100
- - Scans your local AI-agent session transcripts for 50 high-confidence
100
+ - Scans your local AI-agent session transcripts for 56 high-confidence
101
101
  secret patterns: cloud provider keys, private key blocks, OAuth/API
102
102
  tokens, database connection strings, and more. See
103
103
  [`src/patterns.js`](src/patterns.js).
@@ -219,7 +219,7 @@ prompt. There is no recovery if you lose it, so pick one you keep.
219
219
 
220
220
  ## Sources supported today
221
221
 
222
- 43 sources, real-install-verified for Claude Code and its config family,
222
+ 44 sources, real-install-verified for Claude Code and its config family,
223
223
  multi-source-corroborated for the rest (Cursor, Codex CLI, Cline, Windsurf,
224
224
  Gemini CLI, Copilot, and 30+ more). Full list, what "corroborated" means,
225
225
  and how to add one: [docs/sources.md](docs/sources.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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/patterns.js CHANGED
@@ -46,6 +46,46 @@ const PATTERNS = [
46
46
  // paste sk_live at go-live.
47
47
  { id: "stripe_test_key", label: "Stripe API key (test mode)", confidence: "high",
48
48
  re: /\b(sk|rk)_test_[A-Za-z0-9]{20,250}\b/g },
49
+ // Stripe webhook signing secret. Found missing by reading TruffleHog's
50
+ // own open-issue tracker (trufflesecurity/trufflehog#4711 and #4609,
51
+ // both open, both unaddressed) -- a real, disclosed gap, not unique to
52
+ // residoo: neither TruffleHog nor gitleaks (checked gitleaks.toml
53
+ // directly) has this rule either, and no exact-length spec is published
54
+ // anywhere found (Stripe's own docs name the whsec_ prefix but not a
55
+ // length). Shipped anyway on the strength of the prefix alone: "whsec_"
56
+ // is distinctive enough on its own that a generous length bound carries
57
+ // negligible false-positive risk even without a confirmed exact count,
58
+ // unlike a short/generic prefix where that same generosity would matter.
59
+ { id: "stripe_webhook_secret", label: "Stripe webhook signing secret", confidence: "high",
60
+ re: /\bwhsec_[A-Za-z0-9]{24,64}\b/g },
61
+ // Azure AD (Entra ID) client secret. Found missing by cross-checking
62
+ // gitleaks' own open-issue tracker (gitleaks/gitleaks#1687), which
63
+ // asked for Azure coverage generally -- gitleaks itself already has a
64
+ // rule (id "azure-ad-client-secret" in its own gitleaks.toml), so this
65
+ // is adapted directly from that battle-tested pattern rather than
66
+ // designed from scratch: 2-4 chars, one digit, the literal "Q~"
67
+ // marker, then 28-36 more chars. No capture group (unlike gitleaks'
68
+ // Go regex), to match every other rule in this file using the whole
69
+ // match as the value; zero-width lookaround used instead of gitleaks'
70
+ // literal delimiter character classes for the same reason -- the
71
+ // charset includes non-word characters (~ .) that a plain \b boundary
72
+ // can't reliably bound. Azure Storage Account keys were investigated
73
+ // and NOT added: they're a bare, unprefixed base64 blob (~88 chars,
74
+ // confirmed via learn.microsoft.com), the same unsafe generic shape
75
+ // already excluded for Weights & Biases' classic key format.
76
+ { id: "azure_ad_client_secret", label: "Azure AD (Entra ID) client secret", confidence: "high",
77
+ re: /(?<![A-Za-z0-9_.~-])[A-Za-z0-9_.~]{2,4}\dQ~[A-Za-z0-9_.~-]{28,36}(?![A-Za-z0-9_.~-])/g },
78
+ // Tailscale auth key. Found missing via gitleaks/gitleaks#1778 (still
79
+ // open there too). No fully authoritative current spec found: Tailscale's
80
+ // own kb/1085/auth-keys page shows an older bare "tskey-<hex>" example,
81
+ // while more recent third-party usage consistently shows a newer
82
+ // "tskey-auth-<id>-<secret>" two-segment form -- genuine format
83
+ // evolution, not a single confirmed shape. Covers both on the strength
84
+ // of the "tskey-" prefix alone, which carries negligible false-positive
85
+ // risk regardless of which era's exact body shape is present, same
86
+ // reasoning as whsec_ above.
87
+ { id: "tailscale_auth_key", label: "Tailscale auth key", confidence: "high",
88
+ re: /\btskey-(?:auth-)?[A-Za-z0-9-]{15,80}\b/g },
49
89
  // The negative lookahead keeps this rule mutually exclusive with anthropic_key
50
90
  // and openrouter_key below — without it, "sk-ant-..." or "sk-or-v1-..." match
51
91
  // BOTH this pattern and the more specific one, and get reported twice under
@@ -272,6 +312,25 @@ const PATTERNS = [
272
312
  // than a doc-confirmed exact count.
273
313
  { id: "posthog_key", label: "PostHog personal API key", confidence: "high",
274
314
  re: /\bphx_[A-Za-z0-9]{40,}\b/g },
315
+ // Claude Code Remote Control session URL -- not a vendor API key, a URL
316
+ // that IS a bearer credential: opening it in a browser grants full
317
+ // read/write/execute access to a live local Claude Code session, no
318
+ // further auth. Anthropic's own docs (code.claude.com/docs/en/remote-control,
319
+ // fetched 2026-09-04) confirm this URL is printed directly into the
320
+ // conversation ("Claude Code also posts the session URL in the
321
+ // conversation") -- i.e. this genuinely lands in the exact transcripts
322
+ // this project scans, not a hypothetical risk. Found via a gitleaks
323
+ // open-issue request (gitleaks/gitleaks#2094) that proposed a
324
+ // `session_<id>` prefix as "illustrative," unconfirmed. That guess was
325
+ // checked against this project's own installed `claude` binary (macOS,
326
+ // `strings /usr/local/bin/claude`) rather than assumed correct: the
327
+ // literal web-URL template is `` `/code/${sessionId}` `` with NO prefix
328
+ // at all, and `sessionId:mqH.randomUUID()` confirms the ID is a
329
+ // standard UUID v4 -- both directly present in the shipped binary's own
330
+ // strings, not inferred. The gitleaks issue's proposed pattern would
331
+ // have MISSED every real instance of this URL.
332
+ { id: "claude_code_remote_control_url", label: "Claude Code Remote Control session URL", confidence: "high",
333
+ re: /\bclaude\.ai\/code\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g },
275
334
  // LangSmith personal access token (lsv2_pt_) / service key (lsv2_sk_).
276
335
  // Found missing by cross-checking agentsweep's own open-issue tracker.
277
336
  // The first segment (32 hex, UUID-shaped) is consistent across every
package/src/rotation.js CHANGED
@@ -203,6 +203,45 @@ const ROTATION_GUIDANCE = {
203
203
  ],
204
204
  revokeNote: "Test mode is not harmless: the key grants full API access to the sandbox account, and its leak marks a workflow that will handle live keys the same way.",
205
205
  },
206
+ // Fetched docs.stripe.com/webhooks (2026-09-04), "Roll endpoint signing
207
+ // secrets periodically" section: the exact steps below are quoted from
208
+ // that section, not inferred.
209
+ stripe_webhook_secret: {
210
+ label: "Stripe webhook signing secret",
211
+ consolePath: "dashboard.stripe.com/webhooks > select the endpoint",
212
+ steps: [
213
+ "Open the endpoint in the Webhooks tab of Workbench",
214
+ "Overflow menu (...) > Roll secret",
215
+ "Choose immediate expiration for a compromised secret, or up to 24h delay to migrate your verification code first (both secrets stay valid during that window)",
216
+ ],
217
+ revokeNote: "A leaked webhook secret alone can't drain funds or read data -- it only lets an attacker forge fake Stripe-Signature headers to a webhook endpoint that trusts them, so this is a spoofing/logic-bypass risk, not an account-access one. Still worth rolling promptly: it's what stands between a webhook handler and forged events.",
218
+ },
219
+ // Fetched learn.microsoft.com (2026-09-04), Azure App Registration
220
+ // secret management docs: steps below are the documented
221
+ // create-update-verify-delete rotation flow, not guessed.
222
+ azure_ad_client_secret: {
223
+ label: "Azure AD (Entra ID) client secret",
224
+ consolePath: "Microsoft Entra ID > App registrations > [app] > Certificates & secrets",
225
+ steps: [
226
+ "Create a new client secret on the same app registration first (New client secret)",
227
+ "Update whatever used the old one with the new value, and verify it works",
228
+ "Only then delete the leaked secret (trash icon next to it), to minimize downtime",
229
+ ],
230
+ revokeNote: "Deletion is immediate. Prefer create-then-delete over delete-then-create if the app can tolerate two live secrets briefly -- it avoids an outage window for whatever depends on this secret.",
231
+ },
232
+ // Fetched tailscale.com/kb/1085/auth-keys (2026-09-04): exact console
233
+ // path and the important revoke-vs-deauthorize distinction are quoted
234
+ // from that page, not inferred.
235
+ tailscale_auth_key: {
236
+ label: "Tailscale auth key",
237
+ consolePath: "console.tailscale.com/admin/settings/keys",
238
+ steps: [
239
+ "Open the Keys page of the admin console",
240
+ "Find the key in the table and select Revoke",
241
+ "If a node already used this key to join your tailnet, also delete that node from the Machines page -- revoking the key alone does not deauthorize nodes already using it",
242
+ ],
243
+ revokeNote: "Revoking is immediate for future use of the key, but any node it already authorized stays connected until separately removed from Machines -- the two are not the same action.",
244
+ },
206
245
  // help.openai.com articles 5112595 and 8304786 exist (surfaced by search)
207
246
  // but the help center serves HTTP 403 to this project's fetcher, so no URL
208
247
  // is shipped: unverifiable end to end fails the bar above.
@@ -772,6 +811,21 @@ const ROTATION_GUIDANCE = {
772
811
  ],
773
812
  revokeNote: "Deletion is immediate; the key stops authenticating on the next request.",
774
813
  },
814
+ // Not a vendor API key with a console to rotate it in -- a session URL
815
+ // that IS the credential. Steps below per code.claude.com/docs/en/
816
+ // remote-control (fetched 2026-09-04): disconnect terminates the URL's
817
+ // access immediately; there is no separate "revoke" step to perform
818
+ // afterward the way there is for a leaked API key.
819
+ claude_code_remote_control_url: {
820
+ label: "Claude Code Remote Control session URL",
821
+ consolePath: "the terminal running the session, or claude.ai/code's session list",
822
+ steps: [
823
+ "In the terminal running the session, run /remote-control again to disconnect (or use the status panel's disconnect option)",
824
+ "If you no longer have terminal access, end the local session entirely -- Remote Control access ends with it",
825
+ "Start a fresh Remote Control session if you still need one; it gets a new, unrelated URL",
826
+ ],
827
+ revokeNote: "Disconnecting invalidates the URL immediately -- your local session keeps running in the terminal either way, only the remote-access link is torn down. There is no separate token to also revoke at a vendor console.",
828
+ },
775
829
  // Fetched docs.langchain.com/langsmith/create-account-api-key
776
830
  // (2026-09-04): both personal access tokens and service keys are
777
831
  // created and managed from the workspace's own API Keys settings page.
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ /**
8
+ * Atlassian Rovo Dev CLI (`acli rovodev`) — Atlassian's terminal coding
9
+ * agent. Closed-source (part of the Atlassian CLI), so unlike continue.js
10
+ * this cannot be grounded in the project's own source code; grounded
11
+ * instead in Atlassian's own support docs, which is the strongest source
12
+ * available for a closed-source tool.
13
+ *
14
+ * VERIFICATION STATUS: not checked against a real install (no `acli` on
15
+ * this machine, no `~/.rovodev` directory). Confirmed via
16
+ * support.atlassian.com/rovo/docs/manage-sessions-in-rovo-dev-cli/
17
+ * (fetched 2026-09-04): "Sessions are stored by default at
18
+ * `~/.rovodev/sessions/`", with two named files per session:
19
+ * `session_context.json` ("full conversation history and context") and
20
+ * `metadata.json` ("session metadata like title, workspace, and fork
21
+ * information"). The docs page does not explicitly state whether these
22
+ * live directly under `sessions/` or one level down per session -- but
23
+ * since the same two filenames are named for every session, and a flat
24
+ * layout would mean every session's `session_context.json` collides on
25
+ * the same path, a per-session subdirectory
26
+ * (`sessions/<session-id>/session_context.json`) is the only layout
27
+ * consistent with the docs as written, not a guess pulled from nowhere.
28
+ * Ships on that reasoning per CONTRIBUTING.md rule 3; `available()`
29
+ * requires only the root `~/.rovodev` directory, so a wrong subdirectory
30
+ * assumption fails as "no sessions found," never a false "nothing here."
31
+ *
32
+ * The config file (`~/.rovodev/config.yml`, also documented) is not
33
+ * scanned: `--project`-mode-style config coverage is deliberately scoped
34
+ * to the handful of tools this project already has a dedicated config
35
+ * reader for, and a single YAML settings file is a small enough surface
36
+ * that adding it as a bespoke one-off here wasn't judged worth the
37
+ * inconsistency with how every other tool's config gets read.
38
+ */
39
+ const ROOT = path.join(os.homedir(), ".rovodev");
40
+ const SESSIONS_DIR = path.join(ROOT, "sessions");
41
+
42
+ function id() { return "atlassian-rovo-dev"; }
43
+ function label() { return "Atlassian Rovo Dev CLI"; }
44
+
45
+ function available() {
46
+ try { return fs.statSync(ROOT).isDirectory(); } catch { return false; }
47
+ }
48
+
49
+ /** Same defensive symlink-following pattern used throughout src/sources -- see continue.js's docstring for the full reasoning. */
50
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
51
+ if (checkFn(dirent)) return true;
52
+ if (!dirent.isSymbolicLink()) return false;
53
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
54
+ }
55
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
56
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
57
+
58
+ function resolveDirState(dirPath) {
59
+ let lst;
60
+ try { lst = fs.lstatSync(dirPath); } catch { return "absent"; }
61
+ if (lst.isDirectory()) return "ok";
62
+ if (lst.isSymbolicLink()) {
63
+ try { return fs.statSync(dirPath).isDirectory() ? "ok" : "broken"; }
64
+ catch { return "broken"; }
65
+ }
66
+ return "absent";
67
+ }
68
+
69
+ function* yieldFileEntry(file, dirent) {
70
+ if (!dirent.isFile()) {
71
+ const resolved = isFileFollowingSymlink(file, dirent);
72
+ if (!resolved) {
73
+ if (dirent.isSymbolicLink()) yield { file, broken: true };
74
+ return;
75
+ }
76
+ }
77
+ let stat;
78
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; return; }
79
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
80
+ }
81
+
82
+ /**
83
+ * Walks sessions/<session-id>/{session_context.json,metadata.json} per the
84
+ * inferred layout above. If a future/different real layout turns out to be
85
+ * flat instead, this also picks up session_context.json/metadata.json
86
+ * sitting directly in sessions/ itself (an entry that fails the
87
+ * is-a-directory check below is just skipped as "not a session dir", never
88
+ * reported broken), so the more conservative of the two guesses degrades
89
+ * gracefully rather than reporting a false all-clear.
90
+ */
91
+ function* files() {
92
+ const state = resolveDirState(SESSIONS_DIR);
93
+ if (state === "broken") { yield { file: SESSIONS_DIR, broken: true }; return; }
94
+ if (state !== "ok") return; // no sessions/ yet — normal, not broken
95
+
96
+ let entries;
97
+ try { entries = fs.readdirSync(SESSIONS_DIR, { withFileTypes: true }); }
98
+ catch { yield { file: SESSIONS_DIR, broken: true }; return; }
99
+
100
+ for (const e of entries) {
101
+ const p = path.join(SESSIONS_DIR, e.name);
102
+
103
+ if (e.name === "session_context.json" || e.name === "metadata.json") {
104
+ yield* yieldFileEntry(p, e);
105
+ continue;
106
+ }
107
+
108
+ if (!isDirFollowingSymlink(p, e)) {
109
+ if (e.isSymbolicLink()) yield { file: p, broken: true };
110
+ continue; // some other stray entry — out of scope, not broken
111
+ }
112
+
113
+ let innerEntries;
114
+ try { innerEntries = fs.readdirSync(p, { withFileTypes: true }); }
115
+ catch { yield { file: p, broken: true }; continue; }
116
+
117
+ for (const ie of innerEntries) {
118
+ if (ie.name !== "session_context.json" && ie.name !== "metadata.json") continue;
119
+ yield* yieldFileEntry(path.join(p, ie.name), ie);
120
+ }
121
+ }
122
+ }
123
+
124
+ const MAX_JSON_DOC_BYTES = 512 * 1024 * 1024; // generous backstop, no real large example to size against — see continue.js's MAX_JSON_DOC_BYTES for the same admitted status
125
+ const READ_TIMEOUT_MS = 60_000;
126
+
127
+ /**
128
+ * Whole-document JSON read with the same streamed-read/timeout/size-cap
129
+ * discipline as every other whole-document reader in src/sources (see
130
+ * continue.js's readJsonDocumentFile for the full reasoning) — reused by
131
+ * pattern, not import, per this project's one-small-self-contained-file
132
+ * convention. A document that isn't valid JSON (corrupted, truncated, or a
133
+ * genuinely different real format than inferred above) is scanned as one
134
+ * raw line rather than discarded.
135
+ */
136
+ async function readLines(file) {
137
+ let stat;
138
+ try { stat = fs.statSync(file); }
139
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
140
+ if (stat.size > MAX_JSON_DOC_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
141
+
142
+ let text = "";
143
+ let tooLarge = false;
144
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
145
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
146
+
147
+ const readCleanly = await new Promise((resolve) => {
148
+ stream.on("data", (chunk) => {
149
+ text += chunk;
150
+ if (!tooLarge && Buffer.byteLength(text, "utf-8") > MAX_JSON_DOC_BYTES) {
151
+ tooLarge = true;
152
+ stream.destroy();
153
+ }
154
+ });
155
+ stream.once("end", () => resolve(true));
156
+ stream.once("error", () => resolve(false));
157
+ });
158
+ clearTimeout(timer);
159
+
160
+ if (tooLarge) return { lines: [], status: "too-large", bytesRead: 0 };
161
+
162
+ const bytesRead = Buffer.byteLength(text, "utf-8");
163
+ if (!readCleanly && bytesRead === 0) return { lines: [], status: "failed", bytesRead: 0 };
164
+ const status = readCleanly ? "complete" : "partial";
165
+ if (text.length === 0) return { lines: [], status, bytesRead };
166
+
167
+ try {
168
+ const parsed = JSON.parse(text);
169
+ // No confirmed internal schema for session_context.json's history
170
+ // shape (closed source, see header) -- scanned as one flattened
171
+ // document rather than decomposed per-message the way continue.js
172
+ // does, so nothing inside it is assumed about that isn't confirmed.
173
+ return { lines: [JSON.stringify(parsed)], status, bytesRead };
174
+ } catch {
175
+ return { lines: [text], status, bytesRead };
176
+ }
177
+ }
178
+
179
+ module.exports = { id, label, available, files, readLines };
@@ -134,6 +134,12 @@ const antigravityCli = require("./antigravity-cli");
134
134
  const kimiCode = require("./kimi-code");
135
135
  const fx = require("./fx");
136
136
 
137
+ const atlassianRovoDev = require("./atlassian-rovo-dev");
138
+ // Sourcegraph Amp investigated and deliberately not included: its own docs
139
+ // describe threads as syncing to ampcode.com "across devices," and no local
140
+ // cache/offline copy of thread content is documented anywhere found — the
141
+ // same cloud-only reasoning as Augment Code/CodeGPT above, not missed.
142
+
137
143
  const ALL_SOURCES = [
138
144
  claudeCode,
139
145
  agentConfigs,
@@ -178,6 +184,7 @@ const ALL_SOURCES = [
178
184
  antigravityCli,
179
185
  kimiCode,
180
186
  fx,
187
+ atlassianRovoDev,
181
188
  ];
182
189
 
183
190
  function availableSources() {