residoo 0.4.11 → 0.4.13
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 +22 -2
- package/package.json +1 -1
- package/src/scan.js +67 -10
package/README.md
CHANGED
|
@@ -47,6 +47,26 @@ scanned here left your machine; residoo makes no network calls.
|
|
|
47
47
|
> trufflehog/betterleaks' verification postures, in
|
|
48
48
|
> [docs/comparison.md](docs/comparison.md).
|
|
49
49
|
|
|
50
|
+
## Benchmark: measured, not claimed
|
|
51
|
+
|
|
52
|
+
A reproducible benchmark against 8 real competing tools, on a synthetic-but-
|
|
53
|
+
pattern-true corpus (72 Claude Code sessions, 45 planted credentials, zero
|
|
54
|
+
real secrets), with live egress monitoring so "no network calls" is
|
|
55
|
+
observed, not just documented. Re-run against every meaningful release,
|
|
56
|
+
most recently v0.4.13:
|
|
57
|
+
|
|
58
|
+
| | residoo | best of the rest |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Distinct credentials found (all claimed classes) | **44/45 (98%)** | agentsweep 33/42 (79%) |
|
|
61
|
+
| Precision (false positives) | **100%** (0 of 54 flags wrong) | gitleaks, whatileaked, trufflehog also 100% |
|
|
62
|
+
| Network egress during the scan | **none-observed** | 3 of 8 tools attempt real outbound calls in their *default* mode (trufflehog: 50 connection attempts to github.com, slack.com, api.anthropic.com, gitlab.com, npmjs.org) |
|
|
63
|
+
|
|
64
|
+
No single blended score, on purpose: a blend would hide exactly the class-
|
|
65
|
+
level differences (base64-wrapped, split-across-lines, JSON-nested) the
|
|
66
|
+
benchmark exists to measure. Full per-class breakdown, fairness rules, and
|
|
67
|
+
reproduction steps: [bench/](bench/). Self-run, pending independent
|
|
68
|
+
reproduction; everything needed to rerun it ships in this repo.
|
|
69
|
+
|
|
50
70
|
## What it does
|
|
51
71
|
|
|
52
72
|
- Scans your local AI-agent session transcripts for 50 high-confidence
|
|
@@ -265,7 +285,7 @@ As a GitHub Action (this repo doubles as a composite action):
|
|
|
265
285
|
```yaml
|
|
266
286
|
steps:
|
|
267
287
|
- uses: actions/checkout@v4
|
|
268
|
-
- uses: dandovdub/residoo@v0.4.
|
|
288
|
+
- uses: dandovdub/residoo@v0.4.13
|
|
269
289
|
```
|
|
270
290
|
|
|
271
291
|
As a pre-commit hook:
|
|
@@ -273,7 +293,7 @@ As a pre-commit hook:
|
|
|
273
293
|
```yaml
|
|
274
294
|
repos:
|
|
275
295
|
- repo: https://github.com/dandovdub/residoo
|
|
276
|
-
rev: v0.4.
|
|
296
|
+
rev: v0.4.13
|
|
277
297
|
hooks:
|
|
278
298
|
- id: residoo
|
|
279
299
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
765
|
+
verifyRows.push([vendor, endpoint,
|
|
766
|
+
[...byValue.entries()].map(([value, { refs }]) => ({ value, resultFinding: refs[0] }))]);
|
|
741
767
|
}
|
|
742
|
-
if (
|
|
743
|
-
const table =
|
|
744
|
-
.map(([vendor, endpoint,
|
|
745
|
-
` ${paint(c.bold, vendor)} ${paint(c.dim, "·")} ${paint(c.dim, endpoint)}\n` +
|
|
746
|
-
|
|
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 };
|