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.
Files changed (50) hide show
  1. package/README.md +225 -46
  2. package/SECURITY.md +29 -22
  3. package/package.json +1 -1
  4. package/src/cli.js +82 -16
  5. package/src/integrity.js +669 -0
  6. package/src/patterns.js +78 -5
  7. package/src/report.js +74 -7
  8. package/src/sources/agent-configs.js +308 -0
  9. package/src/sources/aider.js +361 -0
  10. package/src/sources/amazon-q.js +199 -0
  11. package/src/sources/antigravity-cli.js +155 -0
  12. package/src/sources/cline.js +208 -0
  13. package/src/sources/codebuff.js +295 -0
  14. package/src/sources/codex-cli.js +258 -0
  15. package/src/sources/cody.js +325 -0
  16. package/src/sources/continue.js +408 -0
  17. package/src/sources/copilot-chat.js +272 -0
  18. package/src/sources/copilot-cli.js +300 -0
  19. package/src/sources/crush.js +364 -0
  20. package/src/sources/cursor.js +374 -0
  21. package/src/sources/devin-cli.js +241 -0
  22. package/src/sources/factory-droid.js +153 -0
  23. package/src/sources/fx.js +136 -0
  24. package/src/sources/gemini-cli.js +242 -0
  25. package/src/sources/goose.js +366 -0
  26. package/src/sources/grok-cli.js +267 -0
  27. package/src/sources/hermes.js +282 -0
  28. package/src/sources/index.js +172 -8
  29. package/src/sources/jetbrains-ai-assistant.js +343 -0
  30. package/src/sources/jetbrains-junie.js +292 -0
  31. package/src/sources/kilo-code.js +430 -0
  32. package/src/sources/kimi-code.js +147 -0
  33. package/src/sources/kiro-cli.js +393 -0
  34. package/src/sources/kiro-ide.js +230 -0
  35. package/src/sources/llm.js +328 -0
  36. package/src/sources/mentat.js +143 -0
  37. package/src/sources/open-interpreter.js +224 -0
  38. package/src/sources/openclaw.js +218 -0
  39. package/src/sources/opencode.js +379 -0
  40. package/src/sources/openhands.js +181 -0
  41. package/src/sources/pearai.js +151 -0
  42. package/src/sources/pi-agent.js +130 -0
  43. package/src/sources/qodo-gen.js +189 -0
  44. package/src/sources/qwen-code.js +244 -0
  45. package/src/sources/roo-code.js +239 -0
  46. package/src/sources/trae.js +294 -0
  47. package/src/sources/void.js +273 -0
  48. package/src/sources/warp.js +395 -0
  49. package/src/sources/windsurf.js +256 -0
  50. 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
- const HELP = `residoo — find secrets leaking through your AI agent's session history
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 — which
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 it never modifies or deletes anything that already exists.
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: ${ALL_SOURCES.map((s) => s.label()).join(", ")}
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 no findings.\n");
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 residoo never deletes anything itself.\n`
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}) ciphertext only:\n`);
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 wrong passphrase, or the vault is corrupted.\n");
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 content does not match what was sealed. Do not trust this copy.\n`);
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: ${ALL_SOURCES.map((s) => s.label()).join(", ")}.\n`
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
- process.stdout.write((wantsJson ? renderJson(result) : render(result, { noColor })) + "\n");
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 };