residoo 0.1.0 → 0.2.0
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 +225 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +82 -16
- package/src/integrity.js +669 -0
- package/src/patterns.js +78 -5
- package/src/report.js +74 -7
- package/src/sources/agent-configs.js +308 -0
- package/src/sources/aider.js +361 -0
- package/src/sources/amazon-q.js +199 -0
- package/src/sources/antigravity-cli.js +155 -0
- package/src/sources/cline.js +208 -0
- package/src/sources/codebuff.js +295 -0
- package/src/sources/codex-cli.js +258 -0
- package/src/sources/cody.js +325 -0
- package/src/sources/continue.js +408 -0
- package/src/sources/copilot-chat.js +272 -0
- package/src/sources/copilot-cli.js +300 -0
- package/src/sources/crush.js +364 -0
- package/src/sources/cursor.js +374 -0
- package/src/sources/devin-cli.js +241 -0
- package/src/sources/factory-droid.js +153 -0
- package/src/sources/fx.js +136 -0
- package/src/sources/gemini-cli.js +242 -0
- package/src/sources/goose.js +366 -0
- package/src/sources/grok-cli.js +267 -0
- package/src/sources/hermes.js +282 -0
- package/src/sources/index.js +172 -8
- package/src/sources/jetbrains-ai-assistant.js +343 -0
- package/src/sources/jetbrains-junie.js +292 -0
- package/src/sources/kilo-code.js +430 -0
- package/src/sources/kimi-code.js +147 -0
- package/src/sources/kiro-cli.js +393 -0
- package/src/sources/kiro-ide.js +230 -0
- package/src/sources/llm.js +328 -0
- package/src/sources/mentat.js +143 -0
- package/src/sources/open-interpreter.js +224 -0
- package/src/sources/openclaw.js +218 -0
- package/src/sources/opencode.js +379 -0
- package/src/sources/openhands.js +181 -0
- package/src/sources/pearai.js +151 -0
- package/src/sources/pi-agent.js +130 -0
- package/src/sources/qodo-gen.js +189 -0
- package/src/sources/qwen-code.js +244 -0
- package/src/sources/roo-code.js +239 -0
- package/src/sources/trae.js +294 -0
- package/src/sources/void.js +273 -0
- package/src/sources/warp.js +395 -0
- package/src/sources/windsurf.js +256 -0
- package/src/sources/zed.js +374 -0
package/src/cli.js
CHANGED
|
@@ -3,18 +3,45 @@
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { availableSources, ALL_SOURCES } = require("./sources");
|
|
5
5
|
const { scan, emptyResult } = require("./scan");
|
|
6
|
-
const { render, renderJson } = require("./report");
|
|
6
|
+
const { render, renderIntegrity, renderJson } = require("./report");
|
|
7
|
+
const { checkIntegrity } = require("./integrity");
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A source is unavailable for the ordinary reason (not installed — nothing
|
|
11
|
+
* more to say) far more often than for a reason worth surfacing. A handful
|
|
12
|
+
* of sources — the SQLite-backed ones gated on the built-in `node:sqlite`
|
|
13
|
+
* module (cursor.js, crush.js, cody.js, devin-cli.js, hermes.js, kiro-cli.js,
|
|
14
|
+
* llm.js, trae.js, void.js, warp.js, zed.js) — export the optional
|
|
15
|
+
* `unavailableReason()` for the one case worth calling out: the tool IS
|
|
16
|
+
* installed but this Node runtime is too old to read its database. Every
|
|
17
|
+
* other source can safely omit this export entirely; this stays a no-op for
|
|
18
|
+
* those rather than requiring every adapter to implement it.
|
|
19
|
+
*/
|
|
20
|
+
function sourceStatusLabel(source) {
|
|
21
|
+
const reason = typeof source.unavailableReason === "function" ? source.unavailableReason() : null;
|
|
22
|
+
return reason ? `${source.label()} (${reason})` : source.label();
|
|
23
|
+
}
|
|
24
|
+
function sourceStatusList() {
|
|
25
|
+
return ALL_SOURCES.map(sourceStatusLabel).join(", ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HELP = `residoo: find secrets leaking through your AI agent's session history
|
|
9
29
|
|
|
10
30
|
Coding agents (Claude Code, Cursor, Copilot, ...) write everything you do
|
|
11
|
-
to a local transcript, including file contents your prompts touch
|
|
31
|
+
to a local transcript, including file contents your prompts touch. That
|
|
12
32
|
means real credentials sitting in plaintext on disk, indefinitely, in a
|
|
13
33
|
place nobody thinks to check. residoo scans those transcripts for them.
|
|
14
34
|
|
|
35
|
+
A scan also runs integrity checks over agent config locations, the 2026
|
|
36
|
+
supply-chain campaigns planted persistence exactly there: SessionStart
|
|
37
|
+
hooks, dropper scripts, folder-open tasks, zero-width Unicode instructions
|
|
38
|
+
hidden in memory/rules files. Every auto-executing hook found in the
|
|
39
|
+
checked locations is listed for your review; only published campaign IOCs
|
|
40
|
+
and campaign-shaped behaviors escalate to warnings.
|
|
41
|
+
|
|
15
42
|
Scanning makes NO network calls and changes nothing on disk. Findings are
|
|
16
43
|
redacted in every output format. Sealing (--seal) writes NEW encrypted
|
|
17
|
-
files only
|
|
44
|
+
files only. It never modifies or deletes anything that already exists.
|
|
18
45
|
|
|
19
46
|
Usage:
|
|
20
47
|
residoo scan [options]
|
|
@@ -24,7 +51,11 @@ Scan options:
|
|
|
24
51
|
--json machine-readable output (full detail, still redacted)
|
|
25
52
|
--include-noisy also run broad, false-positive-prone rules
|
|
26
53
|
--include-suppressed also show matches that looked like placeholder/example text
|
|
27
|
-
--fail-on-find exit code 1 if anything is found (for CI)
|
|
54
|
+
--fail-on-find exit code 1 if anything is found (for CI): secret
|
|
55
|
+
findings and integrity WARNINGS count; integrity
|
|
56
|
+
info-level review items do not
|
|
57
|
+
--no-integrity skip the integrity checks (planted hooks, dropper
|
|
58
|
+
files, auto-run tasks, hidden Unicode)
|
|
28
59
|
--no-color disable ANSI colour
|
|
29
60
|
|
|
30
61
|
Seal options (used with scan):
|
|
@@ -49,7 +80,7 @@ Unseal:
|
|
|
49
80
|
|
|
50
81
|
The passphrase is read from RESIDOO_PASSPHRASE, or prompted (hidden) on a TTY.
|
|
51
82
|
|
|
52
|
-
Sources checked on this machine: ${
|
|
83
|
+
Sources checked on this machine: ${sourceStatusList()}
|
|
53
84
|
`;
|
|
54
85
|
|
|
55
86
|
function argValue(args, flag) {
|
|
@@ -73,7 +104,7 @@ async function runSeal(result, args) {
|
|
|
73
104
|
|
|
74
105
|
const filesWithFindings = [...new Set(result.findings.map((f) => f.file))];
|
|
75
106
|
if (filesWithFindings.length === 0) {
|
|
76
|
-
process.stdout.write("Nothing to seal
|
|
107
|
+
process.stdout.write("Nothing to seal: no findings.\n");
|
|
77
108
|
return 0;
|
|
78
109
|
}
|
|
79
110
|
|
|
@@ -93,7 +124,7 @@ async function runSeal(result, args) {
|
|
|
93
124
|
`${(totalSealed / 1024 / 1024).toFixed(1)}MB encrypted.\n` +
|
|
94
125
|
`Originals were NOT touched. Once you've verified a restore works\n` +
|
|
95
126
|
`(residoo unseal ${path.basename(vaultDir)} --restore 0001.sealed --out /tmp/check), removing the\n` +
|
|
96
|
-
`plaintext originals is your call
|
|
127
|
+
`plaintext originals is your call; residoo never deletes anything itself.\n`
|
|
97
128
|
);
|
|
98
129
|
|
|
99
130
|
if (args.includes("--upload-cloudroam")) {
|
|
@@ -104,7 +135,7 @@ async function runSeal(result, args) {
|
|
|
104
135
|
process.stderr.write("--upload-cloudroam needs CLOUDROAM_API_KEY (env), --connector and --bucket.\n");
|
|
105
136
|
return 2;
|
|
106
137
|
}
|
|
107
|
-
process.stdout.write(`\nUploading sealed vault to CloudRoam (${bucket})
|
|
138
|
+
process.stdout.write(`\nUploading sealed vault to CloudRoam (${bucket}), ciphertext only:\n`);
|
|
108
139
|
const uploaded = await uploadVaultToCloudRoam({
|
|
109
140
|
vaultDir,
|
|
110
141
|
baseUrl: process.env.CLOUDROAM_BASE_URL || "https://cloudroam.io",
|
|
@@ -127,7 +158,7 @@ async function runUnseal(args) {
|
|
|
127
158
|
try {
|
|
128
159
|
manifest = openManifest(vaultDir, passphrase);
|
|
129
160
|
} catch {
|
|
130
|
-
process.stderr.write("Could not open vault
|
|
161
|
+
process.stderr.write("Could not open vault: wrong passphrase, or the vault is corrupted.\n");
|
|
131
162
|
return 1;
|
|
132
163
|
}
|
|
133
164
|
|
|
@@ -151,7 +182,7 @@ async function runUnseal(args) {
|
|
|
151
182
|
`verified byte-identical to the original (SHA-256 match).\n`);
|
|
152
183
|
return 0;
|
|
153
184
|
}
|
|
154
|
-
process.stderr.write(`Restored, but verification FAILED
|
|
185
|
+
process.stderr.write(`Restored, but verification FAILED: content does not match what was sealed. Do not trust this copy.\n`);
|
|
155
186
|
return 1;
|
|
156
187
|
}
|
|
157
188
|
|
|
@@ -180,32 +211,67 @@ async function main(argv) {
|
|
|
180
211
|
// color for a later call that never asked for that.
|
|
181
212
|
const noColor = args.includes("--no-color");
|
|
182
213
|
|
|
214
|
+
// Integrity runs by default: a scan that reports "no secrets leaked" while
|
|
215
|
+
// a planted SessionStart hook sits ready to re-leak them next session is
|
|
216
|
+
// an incomplete answer. Only warn-severity findings (verified campaign
|
|
217
|
+
// signatures, unverifiable configs) gate --fail-on-find — info items are
|
|
218
|
+
// the user's own hooks listed for review, and failing CI on those would
|
|
219
|
+
// train people to pass --no-integrity, which is worse than not checking.
|
|
220
|
+
const wantsIntegrity = !args.includes("--no-integrity");
|
|
221
|
+
const integrityWarnCount = (integ) =>
|
|
222
|
+
integ ? integ.findings.filter((f) => f.severity === "warn").length : 0;
|
|
223
|
+
// The integrity checker's inputs are, by its own threat model, attacker-
|
|
224
|
+
// plantable files — a hostile config must not be able to suppress the
|
|
225
|
+
// secrets report by crashing the checker after the scan already ran. A
|
|
226
|
+
// throw degrades to a warn-severity finding: the run stays alive, the
|
|
227
|
+
// failure stays loud (it still gates --fail-on-find), and it is never a
|
|
228
|
+
// silent all-clear.
|
|
229
|
+
const runIntegrity = () => {
|
|
230
|
+
try { return checkIntegrity(); }
|
|
231
|
+
catch (e) {
|
|
232
|
+
const why = String((e && e.message) || e).replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
233
|
+
return {
|
|
234
|
+
findings: [{
|
|
235
|
+
severity: "warn", kind: "integrity-crashed", file: "(integrity checker)",
|
|
236
|
+
detail: `integrity checks crashed (${why}). Config locations are UNVERIFIED, not clean; the secret-scan results are unaffected`,
|
|
237
|
+
}],
|
|
238
|
+
filesChecked: [],
|
|
239
|
+
scopeNote: "Integrity checks did not complete on this run.",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
183
244
|
const sources = availableSources();
|
|
184
245
|
if (sources.length === 0) {
|
|
185
246
|
const empty = emptyResult();
|
|
247
|
+
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
186
248
|
if (wantsJson) {
|
|
187
249
|
// A --json caller (CI, a script piping into jq) must always get valid JSON
|
|
188
250
|
// on stdout, even on the "nothing to scan" path — a plain-text message on
|
|
189
251
|
// stderr with exit 0 silently breaks that contract.
|
|
190
|
-
process.stdout.write(renderJson(empty) + "\n");
|
|
252
|
+
process.stdout.write(renderJson(empty, integrity) + "\n");
|
|
191
253
|
} else {
|
|
192
254
|
process.stderr.write(
|
|
193
255
|
"No known transcript sources found on this machine.\n" +
|
|
194
|
-
`Checked: ${
|
|
256
|
+
`Checked: ${sourceStatusList()}.\n`
|
|
195
257
|
);
|
|
258
|
+
// The integrity checks are not gated on transcript sources existing —
|
|
259
|
+
// a planted repo-level hook in the CWD is exactly as dangerous here.
|
|
260
|
+
if (integrity) process.stdout.write(renderIntegrity(integrity, { noColor }) + "\n");
|
|
196
261
|
}
|
|
197
|
-
return 0;
|
|
262
|
+
return failOnFind && integrityWarnCount(integrity) > 0 ? 1 : 0;
|
|
198
263
|
}
|
|
199
264
|
|
|
200
265
|
const result = await scan({ sources, includeNoisy, includeSuppressed });
|
|
201
|
-
|
|
266
|
+
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
267
|
+
process.stdout.write((wantsJson ? renderJson(result, integrity) : render(result, { noColor, integrity })) + "\n");
|
|
202
268
|
|
|
203
269
|
if (args.includes("--seal")) {
|
|
204
270
|
const sealExit = await runSeal(result, args);
|
|
205
271
|
if (sealExit !== 0) return sealExit;
|
|
206
272
|
}
|
|
207
273
|
|
|
208
|
-
return failOnFind && result.findings.length > 0 ? 1 : 0;
|
|
274
|
+
return failOnFind && (result.findings.length > 0 || integrityWarnCount(integrity) > 0) ? 1 : 0;
|
|
209
275
|
}
|
|
210
276
|
|
|
211
277
|
module.exports = { main };
|