residoo 0.3.7 → 0.3.9
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 +14 -2
- package/package.json +1 -1
- package/src/cli.js +5 -2
- package/src/report.js +81 -2
package/README.md
CHANGED
|
@@ -121,6 +121,18 @@ won't be built into the tool that writes it.
|
|
|
121
121
|
- Redacts everything in its own output. You get a shape and a first/last-4
|
|
122
122
|
preview, never the real value, including in `--json` mode. A decoded or
|
|
123
123
|
rejoined secret is redacted exactly like a plain one.
|
|
124
|
+
- On an interactive terminal, prints who it is and where it lives before
|
|
125
|
+
scanning starts (`residoo v0.3.9 · find secrets your AI coding agent left
|
|
126
|
+
on disk` plus the repo URL), then a live spinner naming the current file
|
|
127
|
+
as it scans. Every report also opens with the exact version and timestamp
|
|
128
|
+
it was run with (`residoo v0.3.9 · scanned 2026-01-01 12:00`; `--json`
|
|
129
|
+
carries the same as `residooVersion`/`scannedAt`), so a report pasted or
|
|
130
|
+
screenshotted later never leaves you guessing which build produced it.
|
|
131
|
+
When there are findings, the report closes with a "Next steps" pointer to
|
|
132
|
+
`--json` and `--seal`. All of the interactive chatter (the intro, the
|
|
133
|
+
spinner) goes to stderr only and is a complete no-op when stdout/stderr
|
|
134
|
+
are piped, redirected, or run in CI, so none of it can ever interleave
|
|
135
|
+
with `--json`/`--sarif` output.
|
|
124
136
|
- `--sarif` emits SARIF 2.1.0 for GitHub code scanning's Security tab and
|
|
125
137
|
inline pull-request annotations, the same format gitleaks/trufflehog/
|
|
126
138
|
agentsweep already speak, so residoo's own Action and pre-commit hook plug
|
|
@@ -317,7 +329,7 @@ As a GitHub Action (this repository doubles as a composite action):
|
|
|
317
329
|
```yaml
|
|
318
330
|
steps:
|
|
319
331
|
- uses: actions/checkout@v4
|
|
320
|
-
- uses: dandovdub/residoo@v0.3.
|
|
332
|
+
- uses: dandovdub/residoo@v0.3.9
|
|
321
333
|
```
|
|
322
334
|
|
|
323
335
|
As a pre-commit hook:
|
|
@@ -325,7 +337,7 @@ As a pre-commit hook:
|
|
|
325
337
|
```yaml
|
|
326
338
|
repos:
|
|
327
339
|
- repo: https://github.com/dandovdub/residoo
|
|
328
|
-
rev: v0.3.
|
|
340
|
+
rev: v0.3.9
|
|
329
341
|
hooks:
|
|
330
342
|
- id: residoo
|
|
331
343
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require("fs");
|
|
|
5
5
|
const crypto = require("crypto");
|
|
6
6
|
const { availableSources, ALL_SOURCES } = require("./sources");
|
|
7
7
|
const { scan, emptyResult } = require("./scan");
|
|
8
|
-
const { render, renderIntegrity, renderJson, renderSarif } = require("./report");
|
|
8
|
+
const { render, renderIntegrity, renderJson, renderSarif, makeProgressReporter, printIntro } = require("./report");
|
|
9
9
|
const { checkIntegrity } = require("./integrity");
|
|
10
10
|
const {
|
|
11
11
|
ROTATION_GUIDANCE, guidanceFor, loadAcks, ackFinding, renderRotation,
|
|
@@ -404,6 +404,7 @@ async function main(argv) {
|
|
|
404
404
|
// mutated env var would leak past this one invocation and silently kill
|
|
405
405
|
// color for a later call that never asked for that.
|
|
406
406
|
const noColor = args.includes("--no-color");
|
|
407
|
+
printIntro(noColor);
|
|
407
408
|
|
|
408
409
|
// Integrity runs by default: a scan that reports "no secrets leaked" while
|
|
409
410
|
// a planted SessionStart hook sits ready to re-leak them next session is
|
|
@@ -494,7 +495,9 @@ async function main(argv) {
|
|
|
494
495
|
return failOnFind && integrityWarnCount(integrity) > 0 ? 1 : 0;
|
|
495
496
|
}
|
|
496
497
|
|
|
497
|
-
const
|
|
498
|
+
const progress = makeProgressReporter(noColor);
|
|
499
|
+
const result = await scan({ sources, includeNoisy, includeSuppressed, onProgress: progress.onProgress });
|
|
500
|
+
progress.stop();
|
|
498
501
|
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
499
502
|
const rotation = renderRotation(result.findings, acks);
|
|
500
503
|
process.stdout.write((wantsSarif
|
package/src/report.js
CHANGED
|
@@ -29,6 +29,66 @@ function ageDays(mtimeMs) {
|
|
|
29
29
|
return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86400000));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Printed once, before scanning starts: what this is, what version, where
|
|
36
|
+
* it lives. Answers exactly the question a first-time (or every-time)
|
|
37
|
+
* reader has before results ever appear, without waiting for the report.
|
|
38
|
+
* Same TTY gate and same stream as the progress spinner below and for the
|
|
39
|
+
* same reason: stderr only, so a --json/--sarif consumer's stdout is
|
|
40
|
+
* untouched, and a complete no-op under redirection, piping, or CI.
|
|
41
|
+
*/
|
|
42
|
+
function printIntro(noColor) {
|
|
43
|
+
if (!process.stderr.isTTY) return;
|
|
44
|
+
const paint = makePaint(noColor);
|
|
45
|
+
const { version } = require("../package.json");
|
|
46
|
+
process.stderr.write(
|
|
47
|
+
paint(c.bold + c.cyan, `residoo v${version}`) +
|
|
48
|
+
paint(c.dim, " · find secrets your AI coding agent left on disk\n") +
|
|
49
|
+
paint(c.dim, "https://github.com/dandovdub/residoo\n\n")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A progress indicator for the scan phase, wired to scan()'s own
|
|
55
|
+
* onProgress callback. Writes to STDERR only, never stdout: --json/--sarif
|
|
56
|
+
* consumers pipe stdout into a parser, and a spinner corrupting that would
|
|
57
|
+
* be a much worse bug than not having one. Gated on stderr actually being a
|
|
58
|
+
* TTY, so it is a complete no-op under redirection, piping, or CI, exactly
|
|
59
|
+
* the contexts where carriage-return spam in a captured log would be
|
|
60
|
+
* useless or actively annoying, not merely invisible. Shows the actual
|
|
61
|
+
* current file (basename only, through the same safeBasename() every other
|
|
62
|
+
* displayed path in this report goes through — control bytes stripped,
|
|
63
|
+
* invisible code points made visible), not just a running count: real
|
|
64
|
+
* signal, not just motion, and genuinely useful if a scan stalls on one
|
|
65
|
+
* huge file. `stop()` clears the line so whatever prints next starts
|
|
66
|
+
* clean.
|
|
67
|
+
*/
|
|
68
|
+
function makeProgressReporter(noColor) {
|
|
69
|
+
if (!process.stderr.isTTY) return { onProgress: null, stop() {} };
|
|
70
|
+
const paint = makePaint(noColor);
|
|
71
|
+
let count = 0;
|
|
72
|
+
let lastWriteMs = 0;
|
|
73
|
+
let lastLineLen = 0;
|
|
74
|
+
let frame = 0;
|
|
75
|
+
const write = (s, visibleLen) => {
|
|
76
|
+
process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r" + s);
|
|
77
|
+
lastLineLen = visibleLen;
|
|
78
|
+
};
|
|
79
|
+
const onProgress = ({ source, file }) => {
|
|
80
|
+
count++;
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - lastWriteMs < 80) return; // throttled: avoid flicker on a fast scan
|
|
83
|
+
lastWriteMs = now;
|
|
84
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
85
|
+
const label = `scanning ${source}… ${count} file${count === 1 ? "" : "s"} ${safeBasename(file)}`;
|
|
86
|
+
write(paint(c.bold + c.cyan, SPINNER_FRAMES[frame]) + " " + paint(c.dim, label), 2 + label.length);
|
|
87
|
+
};
|
|
88
|
+
const stop = () => { if (lastLineLen > 0) process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r"); };
|
|
89
|
+
return { onProgress, stop };
|
|
90
|
+
}
|
|
91
|
+
|
|
32
92
|
// File NAMES are attacker-controllable text headed for a terminal: in
|
|
33
93
|
// --project mode a hostile checkout chooses its own filenames, and a name
|
|
34
94
|
// carrying raw ESC bytes could clear the screen or overwrite the findings
|
|
@@ -160,11 +220,23 @@ function renderIntegrity(integrity, { noColor = false } = {}) {
|
|
|
160
220
|
return lines.join("\n");
|
|
161
221
|
}
|
|
162
222
|
|
|
223
|
+
/** "YYYY-MM-DD HH:MM" in local time — matches the user's own system clock, not UTC. */
|
|
224
|
+
function localTimestamp(d) {
|
|
225
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
226
|
+
return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
163
229
|
function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppressedCount = 0, distinctCounts = {}, unreadableFiles = [] }, { noColor = false, integrity = null, rotation = null } = {}) {
|
|
164
230
|
const paint = makePaint(noColor);
|
|
165
231
|
const lines = [];
|
|
166
232
|
const push = (s = "") => lines.push(s);
|
|
167
233
|
|
|
234
|
+
// Which build ran and when, up front: a report pasted or screenshotted
|
|
235
|
+
// hours later (or a "why don't I see feature X" question) should never
|
|
236
|
+
// require asking "what version were you even running."
|
|
237
|
+
const { version } = require("../package.json");
|
|
238
|
+
push(paint(c.dim, `residoo v${version} · scanned ${localTimestamp(new Date())}`));
|
|
239
|
+
|
|
168
240
|
const suppressedNote = suppressedCount > 0
|
|
169
241
|
? paint(c.dim, ` (${suppressedCount} more matched but looked like placeholder/example text; see --include-suppressed)`)
|
|
170
242
|
: "";
|
|
@@ -255,9 +327,13 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
|
|
|
255
327
|
push(renderIntegrity(integrity, { noColor }));
|
|
256
328
|
}
|
|
257
329
|
|
|
330
|
+
push();
|
|
331
|
+
push(paint(c.bold, "Next steps:"));
|
|
332
|
+
push(` residoo scan --json ${paint(c.dim, "machine-readable output, full detail")}`);
|
|
333
|
+
push(` residoo scan --seal ${paint(c.dim, "encrypt the affected files into a local vault (originals untouched)")}`);
|
|
258
334
|
push();
|
|
259
335
|
push(paint(c.dim, "Values are redacted in this report (first/last 4 characters only). Nothing scanned"));
|
|
260
|
-
push(paint(c.dim, "here left your machine; residoo makes no network calls.
|
|
336
|
+
push(paint(c.dim, "here left your machine; residoo makes no network calls."));
|
|
261
337
|
|
|
262
338
|
return lines.join("\n");
|
|
263
339
|
}
|
|
@@ -271,8 +347,11 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
|
|
|
271
347
|
// way, since it is derived from already-redacted material and is what
|
|
272
348
|
// "residoo ack" takes.
|
|
273
349
|
function renderJson(result, integrity = null, rotation = null) {
|
|
350
|
+
const { version } = require("../package.json");
|
|
274
351
|
return JSON.stringify(
|
|
275
352
|
{
|
|
353
|
+
residooVersion: version,
|
|
354
|
+
scannedAt: new Date().toISOString(),
|
|
276
355
|
summary: {
|
|
277
356
|
findingCount: result.findings.length,
|
|
278
357
|
filesScanned: result.filesScanned,
|
|
@@ -398,4 +477,4 @@ function renderSarif(result) {
|
|
|
398
477
|
}, null, 2);
|
|
399
478
|
}
|
|
400
479
|
|
|
401
|
-
module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif };
|
|
480
|
+
module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif, makeProgressReporter, printIntro };
|