residoo 0.4.11 → 0.4.12

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 (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/scan.js +67 -10
package/README.md CHANGED
@@ -265,7 +265,7 @@ As a GitHub Action (this repo doubles as a composite action):
265
265
  ```yaml
266
266
  steps:
267
267
  - uses: actions/checkout@v4
268
- - uses: dandovdub/residoo@v0.4.11
268
+ - uses: dandovdub/residoo@v0.4.12
269
269
  ```
270
270
 
271
271
  As a pre-commit hook:
@@ -273,7 +273,7 @@ As a pre-commit hook:
273
273
  ```yaml
274
274
  repos:
275
275
  - repo: https://github.com/dandovdub/residoo
276
- rev: v0.4.11
276
+ rev: v0.4.12
277
277
  hooks:
278
278
  - id: residoo
279
279
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
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/scan.js CHANGED
@@ -237,6 +237,16 @@ const VENDOR_EXAMPLE_VALUES = new Set([
237
237
  /** Matches every finding's own `relFile` convention — never the full path. See SECURITY.md. */
238
238
  function safeName(file) { return path.basename(file); }
239
239
 
240
+ // Same format as report.js's own localTimestamp, duplicated rather than
241
+ // imported: this is a 3-line pure function, and report.js is the
242
+ // presentation layer for stdout while this file's own --verify results
243
+ // table is stderr, the same reasoning pairing.js gives for its own small
244
+ // duplicated helper (looksZeroEntropy) rather than cross-importing.
245
+ function localTimestamp(d) {
246
+ const p2 = (n) => String(n).padStart(2, "0");
247
+ return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
248
+ }
249
+
240
250
  /**
241
251
  * Scan every transcript from every available source.
242
252
  *
@@ -705,6 +715,11 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
705
715
  // file while stderr still reaches a real terminal is a real case: `scan
706
716
  // --verify --json > out.json` should still color this table).
707
717
  const paint = makePaint(noColor, process.stderr);
718
+ // Populated inside the disclosure block below (when there's something to
719
+ // verify), then read again once every verify loop below has finished, to
720
+ // print the results table. Declared out here, not inside that block, so
721
+ // it survives to that second read.
722
+ let verifyRows = [];
708
723
  if (verify && anyPending) {
709
724
  // One disclosure, not one per vendor: this used to print a full
710
725
  // "this is a real network request..." paragraph for EACH vendor in
@@ -724,26 +739,37 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
724
739
  // everywhere else in the report — reusing redact() on the same raw
725
740
  // value record() was called with, not a second display convention.
726
741
  // One line per vendor+endpoint header, one indented line per credential.
727
- const rows = [];
742
+ //
743
+ // Each row also carries a `resultFinding` per credential: a direct
744
+ // reference to the finding object applyVerifyResult mutates below (the
745
+ // access-key/secret finding for AWS, the secret finding for
746
+ // PlanetScale/MongoDB Atlas, the finding itself for a simple vendor).
747
+ // Captured now, read after the verify loops run, so the results table
748
+ // further down needs no second vendor-shape dispatch of its own.
749
+ verifyRows = [];
728
750
  if (pendingAwsVerifications.size > 0 && awsAvailable) {
729
- rows.push(["AWS", "sts:get-caller-identity", [...pendingAwsVerifications.keys()].map(redact)]);
751
+ verifyRows.push(["AWS", "sts:get-caller-identity",
752
+ [...pendingAwsVerifications.entries()].map(([value, { refs }]) => ({ value, resultFinding: refs[0].akiaFinding }))]);
730
753
  }
731
754
  if (pendingPlanetScaleVerifications.size > 0) {
732
- rows.push(["PlanetScale", "organizations endpoint", [...pendingPlanetScaleVerifications.keys()].map(redact)]);
755
+ verifyRows.push(["PlanetScale", "organizations endpoint",
756
+ [...pendingPlanetScaleVerifications.entries()].map(([value, { refs }]) => ({ value, resultFinding: refs[0].secretFinding }))]);
733
757
  }
734
758
  if (pendingMongoDbAtlasVerifications.size > 0) {
735
- rows.push(["MongoDB Atlas", "oauth/token endpoint", [...pendingMongoDbAtlasVerifications.keys()].map(redact)]);
759
+ verifyRows.push(["MongoDB Atlas", "oauth/token endpoint",
760
+ [...pendingMongoDbAtlasVerifications.entries()].map(([value, { refs }]) => ({ value, resultFinding: refs[0].secretFinding }))]);
736
761
  }
737
762
  for (const [ruleId, byValue] of pendingSimpleVerifications) {
738
763
  if (byValue.size === 0) continue;
739
764
  const [vendor, endpoint] = SIMPLE_VERIFY_VENDOR_LABEL[ruleId].split("'s ");
740
- rows.push([vendor, endpoint, [...byValue.keys()].map(redact)]);
765
+ verifyRows.push([vendor, endpoint,
766
+ [...byValue.entries()].map(([value, { refs }]) => ({ value, resultFinding: refs[0] }))]);
741
767
  }
742
- if (rows.length > 0) {
743
- const table = rows
744
- .map(([vendor, endpoint, previews]) =>
745
- ` ${paint(c.bold, vendor)} ${paint(c.dim, "·")} ${paint(c.dim, endpoint)}\n` +
746
- previews.map((p) => ` ${p}`).join("\n"))
768
+ if (verifyRows.length > 0) {
769
+ const table = verifyRows
770
+ .map(([vendor, endpoint, credentials]) =>
771
+ ` ${paint(c.bold + c.cyan, vendor)} ${paint(c.dim, "·")} ${paint(c.dim, endpoint)}\n` +
772
+ credentials.map(({ value }) => ` ${redact(value)}`).join("\n"))
747
773
  .join("\n");
748
774
  process.stderr.write(
749
775
  paint(c.yellow + c.bold, "residoo --verify:") +
@@ -801,6 +827,37 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
801
827
  }
802
828
  }
803
829
 
830
+ if (verify && verifyRows.length > 0) {
831
+ // Same vendor/endpoint grouping as the disclosure table above, now
832
+ // showing what each call actually found. An ACTIVE credential is a
833
+ // real, present-tense risk, colored the same red/bold the Rotation
834
+ // section below uses for the identical fact ("rotate immediately");
835
+ // "could not verify" gets the same red/bold too, on purpose, matching
836
+ // this project's fail-safe-direction policy of treating "unknown" as
837
+ // "assume risk" rather than as reassuring silence. Stamped with when
838
+ // this check actually ran, so a report read later (pasted into a
839
+ // ticket, screenshotted) doesn't silently imply "still true right now."
840
+ const checkedAt = localTimestamp(new Date());
841
+ const describeResult = (finding) => {
842
+ if (finding.verified === "active") {
843
+ return paint(c.red + c.bold, `⚠ ACTIVE: real working credential`) + paint(c.dim, ` (checked ${checkedAt})`);
844
+ }
845
+ if (finding.verified === "invalid") {
846
+ return paint(c.green, `✓ inactive: vendor rejected it`) + paint(c.dim, ` (checked ${checkedAt})`);
847
+ }
848
+ return paint(c.red + c.bold, `⚠ could not verify`) + paint(c.dim, ` (checked ${checkedAt}${finding.verifiedDetail ? `: ${finding.verifiedDetail}` : ""})`);
849
+ };
850
+ const resultsTable = verifyRows
851
+ .map(([vendor, endpoint, credentials]) =>
852
+ ` ${paint(c.bold + c.cyan, vendor)} ${paint(c.dim, "·")} ${paint(c.dim, endpoint)}\n` +
853
+ credentials.map(({ value, resultFinding }) => ` ${redact(value)} ${describeResult(resultFinding)}`).join("\n"))
854
+ .join("\n");
855
+ process.stderr.write(
856
+ paint(c.yellow + c.bold, "residoo --verify:") + " results\n\n" +
857
+ resultsTable + "\n\n"
858
+ );
859
+ }
860
+
804
861
  const distinctCounts = {};
805
862
  for (const [ruleId, set] of distinctByRule) distinctCounts[ruleId] = set.size;
806
863
  return { findings, filesScanned, sourcesScanned, bytesScanned, suppressedCount, distinctCounts, unreadableFiles };