residoo 0.12.0 → 0.14.0
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/package.json +1 -1
- package/src/cli.js +15 -1
- package/src/guard.js +42 -7
- package/src/pii.js +96 -0
- package/src/report.js +9 -6
- package/src/rotation.js +36 -0
- package/src/scan.js +37 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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
|
@@ -150,6 +150,14 @@ Scan options:
|
|
|
150
150
|
similar characters (0/O, Y/*) can be misread,
|
|
151
151
|
which breaks an exact-format match, so this is
|
|
152
152
|
best-effort additional coverage, not a guarantee.
|
|
153
|
+
--include-pii also scan for PII: US Social Security Numbers
|
|
154
|
+
(dashed format only), credit card numbers
|
|
155
|
+
(Luhn-validated), and IBANs (checksum-validated).
|
|
156
|
+
A different RISK CATEGORY, not a lower confidence
|
|
157
|
+
bar -- residoo is deliberately credentials-only
|
|
158
|
+
by default. Deliberately excludes bare email/
|
|
159
|
+
phone (too common in ordinary text to meet this
|
|
160
|
+
project's own high-confidence bar even opt-in).
|
|
153
161
|
|
|
154
162
|
Watch:
|
|
155
163
|
residoo watch continuous scanning instead of one snapshot:
|
|
@@ -918,6 +926,12 @@ async function main(argv) {
|
|
|
918
926
|
// exact confirmed image shape this looks for and its honest accuracy
|
|
919
927
|
// limitations.
|
|
920
928
|
const wantsOcr = args.includes("--ocr");
|
|
929
|
+
// --include-pii: a different RISK CATEGORY, not a lower confidence bar
|
|
930
|
+
// (see pii.js) -- residoo is deliberately credentials-only by default;
|
|
931
|
+
// this opts into three checksum-validated categories (SSN, Luhn-valid
|
|
932
|
+
// card numbers, IBAN) rather than the shape-only, much noisier
|
|
933
|
+
// categories (bare email, phone) some competitors also ship.
|
|
934
|
+
const wantsPii = args.includes("--include-pii");
|
|
921
935
|
|
|
922
936
|
// --project [dir]: the dir is optional (CI passes ".", a bare --project
|
|
923
937
|
// means the current directory). null means machine mode.
|
|
@@ -1029,7 +1043,7 @@ async function main(argv) {
|
|
|
1029
1043
|
|
|
1030
1044
|
const progress = makeProgressReporter(noColor);
|
|
1031
1045
|
const result = await scan({
|
|
1032
|
-
sources, includeNoisy, includeSuppressed, verify, noColor, ocr: wantsOcr,
|
|
1046
|
+
sources, includeNoisy, includeSuppressed, verify, noColor, ocr: wantsOcr, includePii: wantsPii,
|
|
1033
1047
|
onProgress: progress.onProgress,
|
|
1034
1048
|
// Clears the spinner's last frame before --verify's own stderr lines
|
|
1035
1049
|
// print; without this the last spinner line sits uncleared on screen
|
package/src/guard.js
CHANGED
|
@@ -153,13 +153,14 @@ const GUARDED_TOOL_NAMES = new Set(["Bash", "Read"]);
|
|
|
153
153
|
*/
|
|
154
154
|
function evaluateToolInput(toolName, toolInput) {
|
|
155
155
|
if (!GUARDED_TOOL_NAMES.has(toolName) || !toolInput || typeof toolInput !== "object") {
|
|
156
|
-
return { block: false, reason: null };
|
|
156
|
+
return { block: false, label: null, reason: null };
|
|
157
157
|
}
|
|
158
158
|
const candidate = toolName === "Bash" ? toolInput.command : toolInput.file_path;
|
|
159
159
|
const label = matchSensitivePath(candidate);
|
|
160
|
-
if (!label) return { block: false, reason: null };
|
|
160
|
+
if (!label) return { block: false, label: null, reason: null };
|
|
161
161
|
return {
|
|
162
162
|
block: true,
|
|
163
|
+
label,
|
|
163
164
|
reason: `residoo guard: this looks like a read of ${label}. Blocked before it could be written to the session transcript. ` +
|
|
164
165
|
`If this is intentional and safe, ask the human to read it themselves, or disable this hook in .claude/settings.json.`,
|
|
165
166
|
};
|
|
@@ -183,20 +184,44 @@ const PROMPT_GUARD_RULES = PATTERNS.filter((r) => r.confidence === "high");
|
|
|
183
184
|
* evaluateToolInput above, not just a copy of the same bar.
|
|
184
185
|
*/
|
|
185
186
|
function evaluatePromptText(promptText) {
|
|
186
|
-
if (typeof promptText !== "string" || !promptText) return { block: false, reason: null };
|
|
187
|
+
if (typeof promptText !== "string" || !promptText) return { block: false, label: null, preview: null, reason: null };
|
|
187
188
|
for (const rule of PROMPT_GUARD_RULES) {
|
|
188
189
|
rule.re.lastIndex = 0;
|
|
189
190
|
const m = rule.re.exec(promptText);
|
|
190
191
|
if (!m) continue;
|
|
191
192
|
const value = m[0];
|
|
192
193
|
if (VENDOR_EXAMPLE_VALUES.has(value) || zeroEntropyTail(value)) continue;
|
|
194
|
+
const preview = redact(value);
|
|
193
195
|
return {
|
|
194
196
|
block: true,
|
|
195
|
-
|
|
197
|
+
label: rule.label,
|
|
198
|
+
preview,
|
|
199
|
+
reason: `residoo guard: this prompt looks like it contains ${rule.label} (${preview}). ` +
|
|
196
200
|
`Blocked before it could be sent. If this is a false positive, rephrase or remove it, or disable this hook in .claude/settings.json.`,
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
|
-
return { block: false, reason: null };
|
|
203
|
+
return { block: false, label: null, preview: null, reason: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Writes one structured audit line to stderr for a block decision --
|
|
208
|
+
* CONTRIBUTING.md's own hard rule (rule 3) names `~/.residoo/rotations.json`
|
|
209
|
+
* as "the only file residoo ever writes outside an explicit --seal...
|
|
210
|
+
* nothing else may claim this carve-out," so this is NOT a new file, the
|
|
211
|
+
* same choice `cred`'s own audit trail already made for the same reason
|
|
212
|
+
* (see src/credRun.js). Durability is the operator's choice: redirect the
|
|
213
|
+
* hook's own stderr at launch if you want it kept, same as `cred`.
|
|
214
|
+
* Never the raw matched value -- `preview` is already redact()'d by the
|
|
215
|
+
* caller (rule 4: no raw value in any log line, ever), and PreToolUse
|
|
216
|
+
* decisions carry no value at all, only a path-pattern label.
|
|
217
|
+
*/
|
|
218
|
+
function logAuditLine(errOutput, { event, label, preview, sessionId, cwd }) {
|
|
219
|
+
try {
|
|
220
|
+
errOutput.write(JSON.stringify({
|
|
221
|
+
ts: new Date().toISOString(), tool: "residoo guard", event, decision: "block",
|
|
222
|
+
label, ...(preview ? { preview } : {}), sessionId: sessionId || null, cwd: cwd || null,
|
|
223
|
+
}) + "\n");
|
|
224
|
+
} catch { /* stderr write failing is never a reason to fail the hook decision itself */ }
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
/**
|
|
@@ -207,9 +232,11 @@ function evaluatePromptText(promptText) {
|
|
|
207
232
|
* response protocol to `output` (default stdout) -- exit code is the
|
|
208
233
|
* caller's job (bin/residoo.js), this returns the intended process exit
|
|
209
234
|
* code instead of calling process.exit itself, matching every other run*
|
|
210
|
-
* function in cli.js.
|
|
235
|
+
* function in cli.js. Every BLOCK decision also gets one structured line
|
|
236
|
+
* on `errOutput` (default stderr) -- see logAuditLine's own docstring for
|
|
237
|
+
* why stderr, never a file.
|
|
211
238
|
*/
|
|
212
|
-
async function runGuard({ input = process.stdin, output = process.stdout } = {}) {
|
|
239
|
+
async function runGuard({ input = process.stdin, output = process.stdout, errOutput = process.stderr } = {}) {
|
|
213
240
|
const chunks = [];
|
|
214
241
|
for await (const chunk of input) chunks.push(chunk);
|
|
215
242
|
const raw = Buffer.concat(chunks.map((c) => (Buffer.isBuffer(c) ? c : Buffer.from(c)))).toString("utf-8");
|
|
@@ -231,6 +258,10 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
|
|
|
231
258
|
if (payload.hook_event_name === "UserPromptSubmit") {
|
|
232
259
|
const decision = evaluatePromptText(payload.prompt);
|
|
233
260
|
if (!decision.block) return 0;
|
|
261
|
+
logAuditLine(errOutput, {
|
|
262
|
+
event: "UserPromptSubmit", label: decision.label, preview: decision.preview,
|
|
263
|
+
sessionId: payload.session_id, cwd: payload.cwd,
|
|
264
|
+
});
|
|
234
265
|
output.write(JSON.stringify({ decision: "block", reason: decision.reason }) + "\n");
|
|
235
266
|
return 0;
|
|
236
267
|
}
|
|
@@ -238,6 +269,10 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
|
|
|
238
269
|
const decision = evaluateToolInput(payload.tool_name, payload.tool_input);
|
|
239
270
|
if (!decision.block) return 0;
|
|
240
271
|
|
|
272
|
+
logAuditLine(errOutput, {
|
|
273
|
+
event: "PreToolUse", label: decision.label,
|
|
274
|
+
sessionId: payload.session_id, cwd: payload.cwd,
|
|
275
|
+
});
|
|
241
276
|
output.write(JSON.stringify({
|
|
242
277
|
hookSpecificOutput: {
|
|
243
278
|
hookEventName: "PreToolUse",
|
package/src/pii.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in PII detection (--include-pii). Named directly by this session's
|
|
5
|
+
* own competitive research into funded AI-DLP vendors (Strac, Cyberhaven,
|
|
6
|
+
* Nightfall) as one of the few concrete, buildable things residoo could
|
|
7
|
+
* adopt without becoming a hosted service -- and independently
|
|
8
|
+
* corroborated by two direct competitors' own shipped detector lists
|
|
9
|
+
* (DidILeak, Medusa), both of which cover PII alongside credentials.
|
|
10
|
+
*
|
|
11
|
+
* Kept entirely separate from PATTERNS/NOISY_PATTERNS in patterns.js on
|
|
12
|
+
* purpose: residoo's stated identity elsewhere in this project is
|
|
13
|
+
* "deliberately credentials-only" (see docs/comparison.md's DidILeak
|
|
14
|
+
* section), and this module doesn't change that default -- it's a
|
|
15
|
+
* separate, explicitly opt-in category a user has to ask for, the same
|
|
16
|
+
* relationship NOISY_PATTERNS already has to the default rule set, just
|
|
17
|
+
* for a different reason (a different RISK CATEGORY, not a lower
|
|
18
|
+
* confidence bar).
|
|
19
|
+
*
|
|
20
|
+
* Only three detectors, deliberately: DidILeak's own shipped list also
|
|
21
|
+
* includes bare email addresses and phone numbers, but both are far too
|
|
22
|
+
* common in ordinary, non-sensitive text (a support email in a comment, a
|
|
23
|
+
* phone number in an error message) to meet this project's own
|
|
24
|
+
* "high-confidence only, a security tool that cries wolf gets
|
|
25
|
+
* uninstalled" bar, opt-in or not -- DidILeak itself rates them "low"/
|
|
26
|
+
* "info" severity for the same reason. The three included here all have a
|
|
27
|
+
* REAL mathematical validator, not just a shape match, which is what
|
|
28
|
+
* keeps false-positive risk low enough to ship even as an additive
|
|
29
|
+
* category: Luhn for card numbers, ISO 7064 MOD 97-10 for IBAN, and the
|
|
30
|
+
* Social Security Administration's own published invalid-range rules for
|
|
31
|
+
* SSNs (no checksum exists for SSNs, hence "medium" confidence there, not
|
|
32
|
+
* "high" -- disclosed, not smoothed over).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Luhn checksum (ISO/IEC 7812-1): the standard validator for payment card numbers. `digits` must already be digits-only. */
|
|
36
|
+
function luhnValid(digits) {
|
|
37
|
+
let sum = 0;
|
|
38
|
+
let alt = false;
|
|
39
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
40
|
+
let d = digits.charCodeAt(i) - 48;
|
|
41
|
+
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
42
|
+
sum += d;
|
|
43
|
+
alt = !alt;
|
|
44
|
+
}
|
|
45
|
+
return sum % 10 === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ISO 7064 MOD 97-10 (the IBAN checksum): move the first 4 characters to
|
|
50
|
+
* the end, map letters to numbers (A=10..Z=35), and the whole numeral must
|
|
51
|
+
* be congruent to 1 mod 97. Computed digit-by-digit since the numeral is
|
|
52
|
+
* far larger than any JS integer.
|
|
53
|
+
*
|
|
54
|
+
* Scope, disclosed rather than assumed complete: this checks the generic
|
|
55
|
+
* ISO 13616 structural rule and the checksum, not each of the ~70
|
|
56
|
+
* IBAN-issuing countries' own exact fixed length (a German IBAN is always
|
|
57
|
+
* 22 characters, a French one always 27) -- a real per-country length
|
|
58
|
+
* table would need to be built and kept current; the checksum alone
|
|
59
|
+
* already rejects the overwhelming majority of non-IBAN digit/letter runs.
|
|
60
|
+
*/
|
|
61
|
+
function ibanValid(iban) {
|
|
62
|
+
if (iban.length < 15 || iban.length > 34) return false;
|
|
63
|
+
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
64
|
+
let mod = 0;
|
|
65
|
+
for (const ch of rearranged) {
|
|
66
|
+
const code = ch.charCodeAt(0);
|
|
67
|
+
const value = code >= 65 && code <= 90 ? code - 55 : code - 48; // A-Z -> 10-35, else digit
|
|
68
|
+
if (value < 0 || value > 35) return false;
|
|
69
|
+
const digits = value >= 10 ? String(value) : ch;
|
|
70
|
+
for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
71
|
+
}
|
|
72
|
+
return mod === 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PII_PATTERNS = [
|
|
76
|
+
// Dashed format only -- a bare 9-digit run is indistinguishable from
|
|
77
|
+
// countless other numbers in a coding-agent transcript (ports, PIDs,
|
|
78
|
+
// timestamps) and would make this rule pure noise. Invalid-range
|
|
79
|
+
// exclusions are the Social Security Administration's own published
|
|
80
|
+
// rules: area 000, 666, and 900-999 were never issued; group 00 and
|
|
81
|
+
// serial 0000 are never valid within an otherwise-real-shaped number.
|
|
82
|
+
{ id: "us_ssn", label: "US Social Security Number", confidence: "medium",
|
|
83
|
+
re: /\b(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/g },
|
|
84
|
+
// Candidate digit runs (13-19 digits, ISO/IEC 7812's own real-world
|
|
85
|
+
// range, optionally space- or dash-separated the way a human actually
|
|
86
|
+
// types a card number) are Luhn-validated below before ever being
|
|
87
|
+
// reported -- the regex alone is not the detector.
|
|
88
|
+
{ id: "credit_card_number", label: "Credit card number (Luhn-validated)", confidence: "high",
|
|
89
|
+
re: /\b\d(?:[ -]?\d){12,18}\b/g,
|
|
90
|
+
validate: (m) => { const d = m.replace(/[ -]/g, ""); return d.length >= 13 && d.length <= 19 && luhnValid(d); } },
|
|
91
|
+
{ id: "iban", label: "IBAN (checksum-validated)", confidence: "high",
|
|
92
|
+
re: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g,
|
|
93
|
+
validate: (m) => ibanValid(m) },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
module.exports = { PII_PATTERNS, luhnValid, ibanValid };
|
package/src/report.js
CHANGED
|
@@ -534,15 +534,18 @@ function renderJson(result, integrity = null, rotation = null) {
|
|
|
534
534
|
rule: f.ruleId, label: f.label, confidence: f.confidence,
|
|
535
535
|
source: f.source, file: f.relFile, line: f.line, preview: f.preview,
|
|
536
536
|
fileMTimeMs: f.fileMTimeMs,
|
|
537
|
-
// Markers for the decode/reconstruct/OCR passes (absent on
|
|
538
|
-
// findings). `encoding` names how the value was wrapped
|
|
539
|
-
// "base64url"); `spanLines` names the adjacent line
|
|
540
|
-
// was reconstructed across; `ocr` means the
|
|
541
|
-
// text at all -- it was read out of a
|
|
542
|
-
// (see ocr.js)
|
|
537
|
+
// Markers for the decode/reconstruct/OCR/PII passes (absent on
|
|
538
|
+
// ordinary findings). `encoding` names how the value was wrapped
|
|
539
|
+
// ("base64" / "base64url"); `spanLines` names the adjacent line
|
|
540
|
+
// pair a split value was reconstructed across; `ocr` means the
|
|
541
|
+
// value was never plain text at all -- it was read out of a
|
|
542
|
+
// pasted or tool-returned image (see ocr.js); `pii` means this is
|
|
543
|
+
// a --include-pii finding, a different risk category from a
|
|
544
|
+
// credential, not a rule from the default set (see pii.js).
|
|
543
545
|
...(f.encoding ? { encoding: f.encoding } : {}),
|
|
544
546
|
...(f.spanLines ? { spanLines: f.spanLines } : {}),
|
|
545
547
|
...(f.ocr ? { ocr: true } : {}),
|
|
548
|
+
...(f.pii ? { pii: true } : {}),
|
|
546
549
|
fingerprint: fingerprintFinding(f),
|
|
547
550
|
// Only present on an --include-suppressed run: says WHY this finding
|
|
548
551
|
// is low-confidence, so a JSON consumer doesn't have to guess.
|
package/src/rotation.js
CHANGED
|
@@ -1146,6 +1146,42 @@ const ROTATION_GUIDANCE = {
|
|
|
1146
1146
|
revokeNote: "Revocation is immediate; anything still using the old token starts failing authentication at once.",
|
|
1147
1147
|
},
|
|
1148
1148
|
|
|
1149
|
+
// ── PII (only reachable via --include-pii) ─────────────────────────────
|
|
1150
|
+
// Framed deliberately differently from every entry above: PII has no
|
|
1151
|
+
// issuer to revoke a credential with, no console to rotate it in. The
|
|
1152
|
+
// real action is "should this data exist in this file at all" and, if it
|
|
1153
|
+
// was genuinely exposed to someone who shouldn't have it, who to tell.
|
|
1154
|
+
us_ssn: {
|
|
1155
|
+
label: "US Social Security Number",
|
|
1156
|
+
consolePath: "There is no vendor console for this -- an SSN can't be rotated the way a credential can.",
|
|
1157
|
+
steps: [
|
|
1158
|
+
"Confirm this is a real SSN and not a placeholder/test value (no checksum exists for SSNs, so this rule has no mathematical validator behind it, unlike the two below)",
|
|
1159
|
+
"Remove it from the transcript file if it doesn't need to be there (residoo scan --seal quarantines the whole file without deleting anything, if you want a reversible first step)",
|
|
1160
|
+
"If it was genuinely exposed to someone who shouldn't have it, that's an identity-theft exposure, not an account compromise -- the affected person (you, a customer, an employee) may want to consider a credit freeze or fraud alert with the credit bureaus, not \"rotate a key\"",
|
|
1161
|
+
],
|
|
1162
|
+
revokeNote: "Unlike every credential rule in this file, there is no revoke/rotate action available at all -- the number itself doesn't change.",
|
|
1163
|
+
},
|
|
1164
|
+
credit_card_number: {
|
|
1165
|
+
label: "Credit card number (Luhn-validated)",
|
|
1166
|
+
consolePath: "The card issuer's own fraud/support line, or your payment processor's dashboard if this is a customer's card, not your own.",
|
|
1167
|
+
steps: [
|
|
1168
|
+
"If it's your own card, most issuers let you freeze or reissue it from their app without waiting for a physical replacement",
|
|
1169
|
+
"If it's a customer's or someone else's card, tell them directly -- they need to contact their own issuer, you can't act on their behalf",
|
|
1170
|
+
"Remove it from the transcript file if it doesn't need to be there",
|
|
1171
|
+
],
|
|
1172
|
+
revokeNote: "This passed a real Luhn checksum, so it is very unlikely to be a random-looking placeholder -- treat it as a real card number until shown otherwise.",
|
|
1173
|
+
},
|
|
1174
|
+
iban: {
|
|
1175
|
+
label: "IBAN (checksum-validated)",
|
|
1176
|
+
consolePath: "The account holder's own bank.",
|
|
1177
|
+
steps: [
|
|
1178
|
+
"An IBAN alone (without the account holder's separate authentication, e.g. a SEPA mandate) can't usually initiate a transfer by itself, but it's still a real bank account identifier -- treat exposure as a privacy issue even where it isn't immediately a fraud one",
|
|
1179
|
+
"If it's your own account and you're concerned, your bank can advise on IBAN-specific fraud monitoring",
|
|
1180
|
+
"Remove it from the transcript file if it doesn't need to be there",
|
|
1181
|
+
],
|
|
1182
|
+
revokeNote: "This passed the real ISO 7064 MOD 97-10 checksum every valid IBAN must satisfy, so it is very unlikely to be a random-looking placeholder.",
|
|
1183
|
+
},
|
|
1184
|
+
|
|
1149
1185
|
// ── NOISY_PATTERNS (only reachable via --include-noisy) ───────────────
|
|
1150
1186
|
generic_password_assignment: {
|
|
1151
1187
|
label: "Password assignment (noisy rule)",
|
package/src/scan.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const { PATTERNS, NOISY_PATTERNS, redact } = require("./patterns");
|
|
5
5
|
const { findDecodedMatches, findBoundaryMatches, contentProjection } = require("./decode");
|
|
6
6
|
const { isTesseractAvailable, extractImageBlocks, ocrImageBase64 } = require("./ocr");
|
|
7
|
+
const { PII_PATTERNS } = require("./pii");
|
|
7
8
|
const { findPairedSecret, findNearbyCandidate } = require("./pairing");
|
|
8
9
|
const { looksRandom } = require("./rarity");
|
|
9
10
|
const { decodeJwtExpiryMs } = require("./jwtExpiry");
|
|
@@ -272,7 +273,7 @@ function localTimestamp(d) {
|
|
|
272
273
|
* absolute path can itself carry a username or a project name the rest of
|
|
273
274
|
* this report is careful never to print.
|
|
274
275
|
*/
|
|
275
|
-
async function scan({ sources, includeNoisy = false, includeSuppressed = false, onProgress = null, verify = false, verifyOnlyFingerprint = null, onBeforeVerify = null, noColor = false, ocr = false } = {}) {
|
|
276
|
+
async function scan({ sources, includeNoisy = false, includeSuppressed = false, onProgress = null, verify = false, verifyOnlyFingerprint = null, onBeforeVerify = null, noColor = false, ocr = false, includePii = false } = {}) {
|
|
276
277
|
const rules = includeNoisy ? PATTERNS.concat(NOISY_PATTERNS) : PATTERNS;
|
|
277
278
|
// --ocr: checked once, not per line/image -- isTesseractAvailable shells
|
|
278
279
|
// out, and this scan can touch thousands of lines. ocrRequestedButMissing
|
|
@@ -600,6 +601,34 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
|
|
|
600
601
|
}
|
|
601
602
|
};
|
|
602
603
|
|
|
604
|
+
// --include-pii: a completely separate pass from every rule above, on
|
|
605
|
+
// purpose -- PII (see pii.js) is a different RISK CATEGORY from a
|
|
606
|
+
// credential, not just a lower-confidence version of one, so it gets its
|
|
607
|
+
// own opt-in flag and its own pass rather than being folded into `rules`.
|
|
608
|
+
// None of matchLine's AWS-pairing/verification machinery applies to PII
|
|
609
|
+
// at all, so this mirrors decodeLine/ocrLine's simpler shape, not
|
|
610
|
+
// matchLine's. `validate` (Luhn, IBAN's MOD 97-10) runs before a
|
|
611
|
+
// candidate is even considered for suppression -- an invalid checksum
|
|
612
|
+
// is not a "placeholder," it's simply not a match.
|
|
613
|
+
const piiLine = (line, file, relFile, lineNo, mtimeMs) => {
|
|
614
|
+
if (!includePii) return;
|
|
615
|
+
for (const rule of PII_PATTERNS) {
|
|
616
|
+
rule.re.lastIndex = 0;
|
|
617
|
+
let m;
|
|
618
|
+
while ((m = rule.re.exec(line)) !== null) {
|
|
619
|
+
if (rule.validate && !rule.validate(m[0])) continue;
|
|
620
|
+
const before = line.slice(Math.max(0, m.index - CONTEXT_WINDOW), m.index);
|
|
621
|
+
const suppressedReason = suppressionReason(m[0], before, rule.id);
|
|
622
|
+
if (suppressedReason && !includeSuppressed) {
|
|
623
|
+
suppressedCount++;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
record(rule, m[0], relFile, file, lineNo, mtimeMs,
|
|
627
|
+
suppressedReason ? "low" : rule.confidence, suppressedReason, { pii: true });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
603
632
|
// Feature 2: split-line boundary join. A finding here means one credential
|
|
604
633
|
// was split across this line and the next and is contiguous on neither. It
|
|
605
634
|
// is recorded against BOTH contributing lines (each holds a fragment of the
|
|
@@ -717,6 +746,13 @@ async function scan({ sources, includeNoisy = false, includeSuppressed = false,
|
|
|
717
746
|
flagFailed();
|
|
718
747
|
}
|
|
719
748
|
}
|
|
749
|
+
if (includePii) {
|
|
750
|
+
try {
|
|
751
|
+
piiLine(line, file, relFile, i + 1, mtimeMs);
|
|
752
|
+
} catch (err) {
|
|
753
|
+
flagFailed();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
720
756
|
try {
|
|
721
757
|
const content = contentProjection(line);
|
|
722
758
|
// Boundary join with the previous line (2-way splits only; see
|