residoo 0.3.6 → 0.3.8
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 +9 -2
- package/package.json +1 -1
- package/src/cli.js +4 -2
- package/src/report.js +66 -5
package/README.md
CHANGED
|
@@ -121,6 +121,13 @@ 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
|
+
- Every report opens with the exact version and timestamp it was run with
|
|
125
|
+
(`residoo v0.3.8 · scanned 2026-01-01 12:00`; `--json` carries the same
|
|
126
|
+
as `residooVersion`/`scannedAt`), so a report pasted or screenshotted
|
|
127
|
+
later never leaves you guessing which build produced it. On an
|
|
128
|
+
interactive terminal, a lightweight spinner shows scan progress on
|
|
129
|
+
stderr; it is a complete no-op when stdout/stderr are piped, redirected,
|
|
130
|
+
or run in CI, so it can never interleave with `--json`/`--sarif` output.
|
|
124
131
|
- `--sarif` emits SARIF 2.1.0 for GitHub code scanning's Security tab and
|
|
125
132
|
inline pull-request annotations, the same format gitleaks/trufflehog/
|
|
126
133
|
agentsweep already speak, so residoo's own Action and pre-commit hook plug
|
|
@@ -317,7 +324,7 @@ As a GitHub Action (this repository doubles as a composite action):
|
|
|
317
324
|
```yaml
|
|
318
325
|
steps:
|
|
319
326
|
- uses: actions/checkout@v4
|
|
320
|
-
- uses: dandovdub/residoo@v0.3.
|
|
327
|
+
- uses: dandovdub/residoo@v0.3.8
|
|
321
328
|
```
|
|
322
329
|
|
|
323
330
|
As a pre-commit hook:
|
|
@@ -325,7 +332,7 @@ As a pre-commit hook:
|
|
|
325
332
|
```yaml
|
|
326
333
|
repos:
|
|
327
334
|
- repo: https://github.com/dandovdub/residoo
|
|
328
|
-
rev: v0.3.
|
|
335
|
+
rev: v0.3.8
|
|
329
336
|
hooks:
|
|
330
337
|
- id: residoo
|
|
331
338
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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 } = require("./report");
|
|
9
9
|
const { checkIntegrity } = require("./integrity");
|
|
10
10
|
const {
|
|
11
11
|
ROTATION_GUIDANCE, guidanceFor, loadAcks, ackFinding, renderRotation,
|
|
@@ -494,7 +494,9 @@ async function main(argv) {
|
|
|
494
494
|
return failOnFind && integrityWarnCount(integrity) > 0 ? 1 : 0;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
-
const
|
|
497
|
+
const progress = makeProgressReporter();
|
|
498
|
+
const result = await scan({ sources, includeNoisy, includeSuppressed, onProgress: progress.onProgress });
|
|
499
|
+
progress.stop();
|
|
498
500
|
const integrity = wantsIntegrity ? runIntegrity() : null;
|
|
499
501
|
const rotation = renderRotation(result.findings, acks);
|
|
500
502
|
process.stdout.write((wantsSarif
|
package/src/report.js
CHANGED
|
@@ -29,6 +29,43 @@ 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
|
+
* A minimal progress indicator for the scan phase, wired to scan()'s own
|
|
36
|
+
* onProgress callback. Writes to STDERR only, never stdout: --json/--sarif
|
|
37
|
+
* consumers pipe stdout into a parser, and a spinner corrupting that would
|
|
38
|
+
* be a much worse bug than not having one. Gated on stderr actually being a
|
|
39
|
+
* TTY, so it is a complete no-op under redirection, piping, or CI, exactly
|
|
40
|
+
* the contexts where carriage-return spam in a captured log would be
|
|
41
|
+
* useless or actively annoying, not merely invisible. `stop()` clears the
|
|
42
|
+
* line so whatever prints next (the report, on stdout, is unaffected
|
|
43
|
+
* either way since this never touched stdout, but a plain-text stderr
|
|
44
|
+
* reader watching live should not see a stale line lingering) starts
|
|
45
|
+
* clean.
|
|
46
|
+
*/
|
|
47
|
+
function makeProgressReporter() {
|
|
48
|
+
if (!process.stderr.isTTY) return { onProgress: null, stop() {} };
|
|
49
|
+
let count = 0;
|
|
50
|
+
let lastWriteMs = 0;
|
|
51
|
+
let lastLineLen = 0;
|
|
52
|
+
let frame = 0;
|
|
53
|
+
const write = (s) => {
|
|
54
|
+
process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r" + s);
|
|
55
|
+
lastLineLen = s.length;
|
|
56
|
+
};
|
|
57
|
+
const onProgress = ({ source }) => {
|
|
58
|
+
count++;
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now - lastWriteMs < 80) return; // throttled: avoid flicker on a fast scan
|
|
61
|
+
lastWriteMs = now;
|
|
62
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
63
|
+
write(`${SPINNER_FRAMES[frame]} scanning… ${count} file${count === 1 ? "" : "s"} checked (${source})`);
|
|
64
|
+
};
|
|
65
|
+
const stop = () => { if (lastLineLen > 0) process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r"); };
|
|
66
|
+
return { onProgress, stop };
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
// File NAMES are attacker-controllable text headed for a terminal: in
|
|
33
70
|
// --project mode a hostile checkout chooses its own filenames, and a name
|
|
34
71
|
// carrying raw ESC bytes could clear the screen or overwrite the findings
|
|
@@ -160,11 +197,23 @@ function renderIntegrity(integrity, { noColor = false } = {}) {
|
|
|
160
197
|
return lines.join("\n");
|
|
161
198
|
}
|
|
162
199
|
|
|
200
|
+
/** "YYYY-MM-DD HH:MM" in local time — matches the user's own system clock, not UTC. */
|
|
201
|
+
function localTimestamp(d) {
|
|
202
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
203
|
+
return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
163
206
|
function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppressedCount = 0, distinctCounts = {}, unreadableFiles = [] }, { noColor = false, integrity = null, rotation = null } = {}) {
|
|
164
207
|
const paint = makePaint(noColor);
|
|
165
208
|
const lines = [];
|
|
166
209
|
const push = (s = "") => lines.push(s);
|
|
167
210
|
|
|
211
|
+
// Which build ran and when, up front: a report pasted or screenshotted
|
|
212
|
+
// hours later (or a "why don't I see feature X" question) should never
|
|
213
|
+
// require asking "what version were you even running."
|
|
214
|
+
const { version } = require("../package.json");
|
|
215
|
+
push(paint(c.dim, `residoo v${version} · scanned ${localTimestamp(new Date())}`));
|
|
216
|
+
|
|
168
217
|
const suppressedNote = suppressedCount > 0
|
|
169
218
|
? paint(c.dim, ` (${suppressedCount} more matched but looked like placeholder/example text; see --include-suppressed)`)
|
|
170
219
|
: "";
|
|
@@ -197,7 +246,15 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
|
|
|
197
246
|
byRule.get(f.ruleId).items.push(f);
|
|
198
247
|
}
|
|
199
248
|
const byFile = new Map();
|
|
200
|
-
for (const f of findings)
|
|
249
|
+
for (const f of findings) {
|
|
250
|
+
const entry = byFile.get(f.file) || { count: 0, mtimeMs: f.fileMTimeMs };
|
|
251
|
+
entry.count++;
|
|
252
|
+
// Newest mtime wins if a file's own findings ever carried different
|
|
253
|
+
// values (they should not, mtimeMs is a per-file stat, but a defensive
|
|
254
|
+
// max here costs nothing and avoids depending on finding order).
|
|
255
|
+
if (f.fileMTimeMs > entry.mtimeMs) entry.mtimeMs = f.fileMTimeMs;
|
|
256
|
+
byFile.set(f.file, entry);
|
|
257
|
+
}
|
|
201
258
|
const oldest = findings.reduce((a, b) => (b.fileMTimeMs < a ? b.fileMTimeMs : a), Date.now());
|
|
202
259
|
const newest = findings.reduce((a, b) => (b.fileMTimeMs > a ? b.fileMTimeMs : a), 0);
|
|
203
260
|
|
|
@@ -230,9 +287,9 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
|
|
|
230
287
|
|
|
231
288
|
push();
|
|
232
289
|
push(paint(c.bold, "By file:"));
|
|
233
|
-
const fileRows = [...byFile.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15);
|
|
234
|
-
for (const [file, count] of fileRows) {
|
|
235
|
-
push(` ${String(count).padStart(4)} ${paint(c.cyan, safeBasename(file))}`);
|
|
290
|
+
const fileRows = [...byFile.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, 15);
|
|
291
|
+
for (const [file, { count, mtimeMs }] of fileRows) {
|
|
292
|
+
push(` ${String(count).padStart(4)} ${paint(c.dim, `~${String(ageDays(mtimeMs)).padStart(2)}d old`)} ${paint(c.cyan, safeBasename(file))}`);
|
|
236
293
|
}
|
|
237
294
|
if (byFile.size > fileRows.length) push(paint(c.dim, ` … and ${byFile.size - fileRows.length} more file(s)`));
|
|
238
295
|
|
|
@@ -263,8 +320,11 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
|
|
|
263
320
|
// way, since it is derived from already-redacted material and is what
|
|
264
321
|
// "residoo ack" takes.
|
|
265
322
|
function renderJson(result, integrity = null, rotation = null) {
|
|
323
|
+
const { version } = require("../package.json");
|
|
266
324
|
return JSON.stringify(
|
|
267
325
|
{
|
|
326
|
+
residooVersion: version,
|
|
327
|
+
scannedAt: new Date().toISOString(),
|
|
268
328
|
summary: {
|
|
269
329
|
findingCount: result.findings.length,
|
|
270
330
|
filesScanned: result.filesScanned,
|
|
@@ -277,6 +337,7 @@ function renderJson(result, integrity = null, rotation = null) {
|
|
|
277
337
|
findings: result.findings.map((f) => ({
|
|
278
338
|
rule: f.ruleId, label: f.label, confidence: f.confidence,
|
|
279
339
|
source: f.source, file: f.relFile, line: f.line, preview: f.preview,
|
|
340
|
+
fileMTimeMs: f.fileMTimeMs,
|
|
280
341
|
// Markers for the two decode/reconstruct passes (absent on ordinary
|
|
281
342
|
// findings). `encoding` names how the value was wrapped ("base64" /
|
|
282
343
|
// "base64url"); `spanLines` names the adjacent line pair a split value
|
|
@@ -389,4 +450,4 @@ function renderSarif(result) {
|
|
|
389
450
|
}, null, 2);
|
|
390
451
|
}
|
|
391
452
|
|
|
392
|
-
module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif };
|
|
453
|
+
module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif, makeProgressReporter };
|