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 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.6
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.6
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.6",
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 result = await scan({ sources, includeNoisy, includeSuppressed });
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) byFile.set(f.file, (byFile.get(f.file) || 0) + 1);
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 };