residoo 0.8.2 → 0.8.4
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 +2 -2
- package/package.json +1 -1
- package/src/patterns.js +37 -0
- package/src/rotation.js +39 -0
- package/src/sources/atlassian-rovo-dev.js +179 -0
- package/src/sources/index.js +7 -0
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
|
|
100
|
+
- Scans your local AI-agent session transcripts for 53 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
|
-
|
|
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
|
+
"version": "0.8.4",
|
|
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,18 @@ 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 },
|
|
49
61
|
// The negative lookahead keeps this rule mutually exclusive with anthropic_key
|
|
50
62
|
// and openrouter_key below — without it, "sk-ant-..." or "sk-or-v1-..." match
|
|
51
63
|
// BOTH this pattern and the more specific one, and get reported twice under
|
|
@@ -272,6 +284,31 @@ const PATTERNS = [
|
|
|
272
284
|
// than a doc-confirmed exact count.
|
|
273
285
|
{ id: "posthog_key", label: "PostHog personal API key", confidence: "high",
|
|
274
286
|
re: /\bphx_[A-Za-z0-9]{40,}\b/g },
|
|
287
|
+
// LangSmith personal access token (lsv2_pt_) / service key (lsv2_sk_).
|
|
288
|
+
// Found missing by cross-checking agentsweep's own open-issue tracker.
|
|
289
|
+
// The first segment (32 hex, UUID-shaped) is consistent across every
|
|
290
|
+
// real example checked (docs.langchain.com and independent citations of
|
|
291
|
+
// it); the trailing segment's exact length is NOT confirmed by any
|
|
292
|
+
// primary source found (two secondary citations of the same example
|
|
293
|
+
// key disagreed by one character), so it's a generous bound, not a
|
|
294
|
+
// doc-confirmed exact count -- same honesty standard as posthog_key
|
|
295
|
+
// above. "lsv2_" itself is distinctive enough that this bound doesn't
|
|
296
|
+
// meaningfully raise false-positive risk either way.
|
|
297
|
+
{ id: "langsmith_key", label: "LangSmith API key", confidence: "high",
|
|
298
|
+
re: /\blsv2_(?:pt|sk)_[a-f0-9]{32}_[a-f0-9]{6,16}\b/g },
|
|
299
|
+
// Resend API key (re_). Found missing the same way. No primary source
|
|
300
|
+
// publishes an exact length spec; the one real example seen (Resend's
|
|
301
|
+
// own docs) is an 8-char id segment + a 24-char secret segment, both
|
|
302
|
+
// mixed-case alphanumeric, joined by one underscore. Bounded generously
|
|
303
|
+
// around that shape rather than pinned to it exactly. The two-segment,
|
|
304
|
+
// underscore-joined, both-high-entropy structure is deliberately what
|
|
305
|
+
// makes this safe as a DEFAULT (non-noisy) rule despite the short,
|
|
306
|
+
// otherwise-generic "re_" prefix: tested against realistic re_-prefixed
|
|
307
|
+
// code identifiers (re_try, re_send, re_connect, re_validate...) and
|
|
308
|
+
// none match, since none of them have a second underscore-delimited
|
|
309
|
+
// random-looking segment.
|
|
310
|
+
{ id: "resend_key", label: "Resend API key", confidence: "high",
|
|
311
|
+
re: /\bre_[A-Za-z0-9]{6,12}_[A-Za-z0-9]{18,32}\b/g },
|
|
275
312
|
];
|
|
276
313
|
|
|
277
314
|
/**
|
package/src/rotation.js
CHANGED
|
@@ -203,6 +203,19 @@ 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
|
+
},
|
|
206
219
|
// help.openai.com articles 5112595 and 8304786 exist (surfaced by search)
|
|
207
220
|
// but the help center serves HTTP 403 to this project's fetcher, so no URL
|
|
208
221
|
// is shipped: unverifiable end to end fails the bar above.
|
|
@@ -772,6 +785,32 @@ const ROTATION_GUIDANCE = {
|
|
|
772
785
|
],
|
|
773
786
|
revokeNote: "Deletion is immediate; the key stops authenticating on the next request.",
|
|
774
787
|
},
|
|
788
|
+
// Fetched docs.langchain.com/langsmith/create-account-api-key
|
|
789
|
+
// (2026-09-04): both personal access tokens and service keys are
|
|
790
|
+
// created and managed from the workspace's own API Keys settings page.
|
|
791
|
+
langsmith_key: {
|
|
792
|
+
label: "LangSmith API key",
|
|
793
|
+
consolePath: "smith.langchain.com > Settings > API Keys",
|
|
794
|
+
steps: [
|
|
795
|
+
"Open API Keys under the workspace's settings",
|
|
796
|
+
"Delete the leaked key (personal access token or service key)",
|
|
797
|
+
"Create a replacement and update whatever used the old one",
|
|
798
|
+
],
|
|
799
|
+
revokeNote: "Deletion is immediate; the key stops authenticating on the next request.",
|
|
800
|
+
},
|
|
801
|
+
// Fetched resend.com/docs/dashboard/api-keys/introduction (2026-09-04):
|
|
802
|
+
// keys are removed from the dashboard's API Keys list (More options ->
|
|
803
|
+
// Remove API key), or programmatically via the DELETE /api-keys endpoint.
|
|
804
|
+
resend_key: {
|
|
805
|
+
label: "Resend API key",
|
|
806
|
+
consolePath: "resend.com/api-keys",
|
|
807
|
+
steps: [
|
|
808
|
+
"Open the API Keys list in the Resend dashboard",
|
|
809
|
+
"Click More options on the leaked key, then Remove API key",
|
|
810
|
+
"Create a replacement and update whatever used the old one",
|
|
811
|
+
],
|
|
812
|
+
revokeNote: "Deletion is immediate; the key stops authenticating on the next request.",
|
|
813
|
+
},
|
|
775
814
|
|
|
776
815
|
// ── NOISY_PATTERNS (only reachable via --include-noisy) ───────────────
|
|
777
816
|
generic_password_assignment: {
|
|
@@ -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 };
|
package/src/sources/index.js
CHANGED
|
@@ -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() {
|