@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.21.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/CHANGELOG.md +12 -0
- package/README.md +22 -2
- package/bin/sensitive-data-sanitizer.js +50 -28
- package/docs/API.md +355 -16
- package/docs/COVERAGE.md +19 -9
- package/docs/RULES.md +218 -0
- package/examples/second-pass-placeholders.mjs +28 -0
- package/package.json +12 -3
- package/src/allow-list.js +93 -0
- package/src/confidence.js +24 -9
- package/src/context.js +287 -40
- package/src/credential-spans.js +180 -0
- package/src/credentials-entry.d.ts +23 -0
- package/src/credentials-entry.js +62 -0
- package/src/credentials.js +51 -0
- package/src/crypto-addresses.js +74 -0
- package/src/detection.js +87 -41
- package/src/document-person-cues.js +1 -1
- package/src/encoded.js +137 -51
- package/src/encrypt.js +95 -0
- package/src/engines.js +33 -10
- package/src/entities.js +4 -3
- package/src/exempt.js +72 -0
- package/src/fake-memory.js +32 -0
- package/src/fake.js +33 -29
- package/src/features.js +29 -0
- package/src/files.js +57 -12
- package/src/gitleaks.js +82 -27
- package/src/html-tags.js +8 -0
- package/src/identifier-detector.js +36 -0
- package/src/identity-types.js +63 -0
- package/src/identity.js +3 -47
- package/src/index.d.ts +157 -3
- package/src/index.js +13 -0
- package/src/known.js +33 -15
- package/src/national-checks.js +209 -0
- package/src/national.js +310 -92
- package/src/pattern-detector.js +219 -0
- package/src/personal-context.js +3 -2
- package/src/personal-evidence.js +233 -0
- package/src/personal-features.js +39 -0
- package/src/personal.js +66 -10
- package/src/placeholder-types.js +154 -4
- package/src/placeholders.js +127 -45
- package/src/projection.js +78 -26
- package/src/proper-names.js +48 -5
- package/src/prose-words.js +120 -0
- package/src/record-worker.js +1 -0
- package/src/rule-catalog.js +426 -0
- package/src/rules.js +8 -0
- package/src/sanitizer.js +133 -23
- package/src/sarif.js +127 -0
- package/src/stream-worker.js +1 -0
- package/src/strict-attributes.js +48 -4
- package/src/strict.js +1 -1
- package/src/structured.js +2 -0
- package/src/token-checksum.js +49 -0
- package/src/transforms.js +146 -33
- package/src/vendor/betterleaks/LICENSE +21 -0
- package/src/vendor/betterleaks/provenance.json +11 -0
- package/src/vendor/betterleaks/rules-0.json +463 -0
- package/src/vendor/betterleaks/rules-1.json +454 -0
- package/src/vendor/betterleaks/rules-2.json +461 -0
- package/src/vendor/betterleaks/rules-3.json +455 -0
- package/src/vendor/betterleaks/rules-4.json +483 -0
- package/src/vendor/betterleaks/rules-5.json +35 -0
- package/src/vendor/entities/aws.json +1 -1
- package/src/vendor/entities/azure.json +6 -2
- package/src/vendor/entities/google.json +2 -1
- package/src/vocabulary.js +27 -1
- package/src/worker-options.js +1 -0
package/src/gitleaks.js
CHANGED
|
@@ -7,22 +7,46 @@ import r2 from './vendor/gitleaks/rules-2.json' with { type: 'json' };
|
|
|
7
7
|
import r3 from './vendor/gitleaks/rules-3.json' with { type: 'json' };
|
|
8
8
|
import r4 from './vendor/gitleaks/rules-4.json' with { type: 'json' };
|
|
9
9
|
import r5 from './vendor/gitleaks/rules-5.json' with { type: 'json' };
|
|
10
|
+
import b0 from './vendor/betterleaks/rules-0.json' with { type: 'json' };
|
|
11
|
+
import b1 from './vendor/betterleaks/rules-1.json' with { type: 'json' };
|
|
12
|
+
import b2 from './vendor/betterleaks/rules-2.json' with { type: 'json' };
|
|
13
|
+
import b3 from './vendor/betterleaks/rules-3.json' with { type: 'json' };
|
|
14
|
+
import b4 from './vendor/betterleaks/rules-4.json' with { type: 'json' };
|
|
15
|
+
import b5 from './vendor/betterleaks/rules-5.json' with { type: 'json' };
|
|
10
16
|
import { entropy, REDACTED } from './detection.js';
|
|
11
17
|
import { keywordMatcher } from './keywords.js';
|
|
18
|
+
import { tokenChecksum } from './token-checksum.js';
|
|
12
19
|
|
|
13
20
|
export const gitleaksRules = [...r0, ...r1, ...r2, ...r3, ...r4, ...r5];
|
|
21
|
+
// Betterleaks rules whose ids are absent from the frozen Gitleaks set; see
|
|
22
|
+
// scripts/sync-betterleaks.py for the translated filter subset.
|
|
23
|
+
export const betterleaksRules = [...b0, ...b1, ...b2, ...b3, ...b4, ...b5];
|
|
24
|
+
const upstreamRules = [
|
|
25
|
+
...gitleaksRules.map((rule) => ({ rule, name: `gitleaks/${rule.id}` })),
|
|
26
|
+
...betterleaksRules.map((rule) => ({
|
|
27
|
+
rule,
|
|
28
|
+
name: `betterleaks/${rule.id}`,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
14
31
|
const compiled = new Map();
|
|
15
32
|
const linearPatterns = new Map();
|
|
16
33
|
const keywordIndex = keywordMatcher(
|
|
17
|
-
|
|
18
|
-
(
|
|
34
|
+
upstreamRules.flatMap(({ rule }) =>
|
|
35
|
+
(rule.keywords ?? []).map((word) => word.toLowerCase())
|
|
19
36
|
)
|
|
20
37
|
);
|
|
38
|
+
const confidence = { high: 0.95, medium: 0.85, low: 0.75 };
|
|
39
|
+
// re2-wasm has a fixed 16 MiB heap shared by every compiled rule and every
|
|
40
|
+
// subject copy, and an allocation failure aborts the module for the rest of
|
|
41
|
+
// the process. All bundled rules compiled take about 7 MiB, and each exec
|
|
42
|
+
// copies its subject, so keep subjects well below the remaining space and
|
|
43
|
+
// scan larger texts with RE2JS (experiments/issue-128/re2-big-input.mjs).
|
|
44
|
+
export const WASM_SUBJECT_LIMIT = 1024 * 1024;
|
|
21
45
|
|
|
22
46
|
export function detectGitleaks(text, emit, trace) {
|
|
23
47
|
const keywords = keywordIndex(text.toLowerCase());
|
|
24
|
-
const large = Buffer.byteLength(text) >
|
|
25
|
-
for (const rule of
|
|
48
|
+
const large = Buffer.byteLength(text) > WASM_SUBJECT_LIMIT;
|
|
49
|
+
for (const { rule, name } of upstreamRules) {
|
|
26
50
|
if (
|
|
27
51
|
!rule.regex ||
|
|
28
52
|
(rule.keywords?.length &&
|
|
@@ -32,10 +56,10 @@ export function detectGitleaks(text, emit, trace) {
|
|
|
32
56
|
}
|
|
33
57
|
const started = trace ? performance.now() : 0;
|
|
34
58
|
for (const view of ruleViews(text, rule)) {
|
|
35
|
-
scanRule(view.text, view.start, rule, emit, large);
|
|
59
|
+
scanRule(view.text, view.start, { rule, name }, emit, large);
|
|
36
60
|
}
|
|
37
61
|
trace?.({
|
|
38
|
-
rule: rule.id,
|
|
62
|
+
rule: name.startsWith('gitleaks/') ? rule.id : name,
|
|
39
63
|
length: text.length,
|
|
40
64
|
ms: performance.now() - started,
|
|
41
65
|
});
|
|
@@ -118,8 +142,8 @@ function* boundedViews(text, prefix, id) {
|
|
|
118
142
|
}
|
|
119
143
|
}
|
|
120
144
|
|
|
121
|
-
function scanRule(text, base, rule, emit, large) {
|
|
122
|
-
for (const match of ruleMatches(text, rule, large)) {
|
|
145
|
+
function scanRule(text, base, { rule, name }, emit, large) {
|
|
146
|
+
for (const match of ruleMatches(text, name, rule, large)) {
|
|
123
147
|
const secret = capturedSecret(rule, match);
|
|
124
148
|
if (!secret) {
|
|
125
149
|
continue;
|
|
@@ -133,17 +157,25 @@ function scanRule(text, base, rule, emit, large) {
|
|
|
133
157
|
end: base + match.index + last + secret.length,
|
|
134
158
|
type: 'SECRET',
|
|
135
159
|
category: 'credential',
|
|
136
|
-
rule:
|
|
137
|
-
confidence: rule
|
|
160
|
+
rule: name,
|
|
161
|
+
confidence: ruleConfidence(rule, secret),
|
|
138
162
|
});
|
|
139
163
|
}
|
|
140
164
|
}
|
|
141
165
|
|
|
142
|
-
|
|
166
|
+
// A token whose embedded CRC32 checksum matches is structurally valid offline.
|
|
167
|
+
function ruleConfidence(rule, secret) {
|
|
168
|
+
if (tokenChecksum(secret)) {
|
|
169
|
+
return 0.99;
|
|
170
|
+
}
|
|
171
|
+
return confidence[rule.confidence] ?? (rule.entropy ? 0.85 : 0.95);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function* ruleMatches(text, name, rule, large) {
|
|
143
175
|
if (large) {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
yield* linearMatches(text, rule);
|
|
176
|
+
// RE2JS implements the same linear-time syntax without copying the input
|
|
177
|
+
// into WASM. Its indices are UTF-16.
|
|
178
|
+
yield* linearMatches(text, name, rule.regex);
|
|
147
179
|
return;
|
|
148
180
|
}
|
|
149
181
|
// re2-wasm 1.0.2 returns codepoint indices despite its RegExp-like API.
|
|
@@ -157,11 +189,7 @@ function* ruleMatches(text, rule, large) {
|
|
|
157
189
|
offsets.push(offset);
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
|
-
|
|
161
|
-
if (!pattern) {
|
|
162
|
-
pattern = new RE2(rule.regex, 'gu');
|
|
163
|
-
compiled.set(rule.id, pattern);
|
|
164
|
-
}
|
|
192
|
+
const pattern = compiledRule(name, rule.regex);
|
|
165
193
|
pattern.lastIndex = 0;
|
|
166
194
|
for (let attempts = 0; attempts < 8; attempts++) {
|
|
167
195
|
const match = pattern.exec(text);
|
|
@@ -177,16 +205,32 @@ function* ruleMatches(text, rule, large) {
|
|
|
177
205
|
// Keep the original subject: slicing would change anchors and boundaries.
|
|
178
206
|
const resume = offsets ? offsets[pattern.lastIndex] : pattern.lastIndex;
|
|
179
207
|
if (resume !== undefined && resume <= text.length) {
|
|
180
|
-
yield* linearMatches(text, rule, resume);
|
|
208
|
+
yield* linearMatches(text, name, rule.regex, resume);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// One cached re2-wasm object per rule name. re2-wasm objects are never freed,
|
|
213
|
+
// so every caller, tests included, shares this cache.
|
|
214
|
+
export function compiledRule(name, source) {
|
|
215
|
+
let pattern = compiled.get(name);
|
|
216
|
+
if (!pattern) {
|
|
217
|
+
pattern = new RE2(source, 'gu');
|
|
218
|
+
compiled.set(name, pattern);
|
|
181
219
|
}
|
|
220
|
+
return pattern;
|
|
182
221
|
}
|
|
183
222
|
|
|
184
|
-
function
|
|
185
|
-
let linear = linearPatterns.get(
|
|
223
|
+
function linearPattern(key, source) {
|
|
224
|
+
let linear = linearPatterns.get(key);
|
|
186
225
|
if (!linear) {
|
|
187
|
-
linear = RE2JS.compile(
|
|
188
|
-
linearPatterns.set(
|
|
226
|
+
linear = RE2JS.compile(source);
|
|
227
|
+
linearPatterns.set(key, linear);
|
|
189
228
|
}
|
|
229
|
+
return linear;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function* linearMatches(text, name, source, resume = 0) {
|
|
233
|
+
const linear = linearPattern(name, source);
|
|
190
234
|
if (linear.test(text)) {
|
|
191
235
|
for (const match of linear.matchAll(text)) {
|
|
192
236
|
if (match.index >= resume) {
|
|
@@ -200,9 +244,20 @@ function capturedSecret(rule, match) {
|
|
|
200
244
|
const secret = rule.secretGroup
|
|
201
245
|
? match[rule.secretGroup]
|
|
202
246
|
: (match.slice(1).find(Boolean) ?? match[0]);
|
|
203
|
-
return !secret ||
|
|
204
|
-
secret === REDACTED ||
|
|
205
|
-
(rule.entropy && entropy(secret) < rule.entropy)
|
|
247
|
+
return !secret || secret === REDACTED || filtered(rule, secret)
|
|
206
248
|
? undefined
|
|
207
249
|
: secret;
|
|
208
250
|
}
|
|
251
|
+
|
|
252
|
+
// Secret-only Betterleaks filter clauses, translated by the sync script.
|
|
253
|
+
function filtered(rule, secret) {
|
|
254
|
+
const score = rule.entropy ? entropy(secret) : Infinity;
|
|
255
|
+
const test = (source) =>
|
|
256
|
+
linearPattern(`filter:${source}`, source).test(secret);
|
|
257
|
+
return (
|
|
258
|
+
score < rule.entropy ||
|
|
259
|
+
(rule.entropyInclusive && score === rule.entropy) ||
|
|
260
|
+
(rule.reject ?? []).some(test) ||
|
|
261
|
+
!(rule.require ?? []).every(test)
|
|
262
|
+
);
|
|
263
|
+
}
|
package/src/html-tags.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// HTML formatting tags split tokens (`SECRET_</span><span>REST`). Inline tags
|
|
2
|
+
// are dropped and block tags become line breaks. Only presentation attributes
|
|
3
|
+
// are allowed, so other attribute values (URLs, data-*) stay in the view.
|
|
4
|
+
export const inlineTags =
|
|
5
|
+
'abbr|b|cite|code|del|em|font|i|ins|kbd|mark|q|s|samp|small|span|strong|sub|sup|tt|u|var';
|
|
6
|
+
const blockTags =
|
|
7
|
+
'blockquote|br|dd|div|dt|h[1-6]|hr|li|ol|p|pre|table|tbody|td|th|thead|tr|ul';
|
|
8
|
+
export const htmlTag = String.raw`<\/?(?:${inlineTags}|${blockTags})(?:\s+(?:class|dir|lang|style)="[^"<>=]*")*\s*\/?>`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { failure } from './detection.js';
|
|
2
|
+
import { detectNational } from './national.js';
|
|
3
|
+
|
|
4
|
+
const validRule = (rule) =>
|
|
5
|
+
rule &&
|
|
6
|
+
typeof rule.type === 'string' &&
|
|
7
|
+
/^[A-Z][A-Z0-9_]{0,63}$/.test(rule.type) &&
|
|
8
|
+
rule.pattern instanceof RegExp &&
|
|
9
|
+
rule.pattern.global &&
|
|
10
|
+
rule.context instanceof RegExp &&
|
|
11
|
+
!rule.context.global &&
|
|
12
|
+
!rule.context.sticky &&
|
|
13
|
+
typeof rule.checksum === 'function' &&
|
|
14
|
+
['plausible', 'valid'].every(
|
|
15
|
+
(key) => rule[key] === undefined || typeof rule[key] === 'function'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Adapts identifier rules (the `nationalRules` shape) to the Detector port, so
|
|
19
|
+
// callers can add or replace identifiers without forking the built-in table.
|
|
20
|
+
export function createIdentifierDetector(
|
|
21
|
+
rules,
|
|
22
|
+
{ id = 'identifier-rules' } = {}
|
|
23
|
+
) {
|
|
24
|
+
if (!Array.isArray(rules) || rules.length === 0 || !rules.every(validRule)) {
|
|
25
|
+
throw failure('ERR_CONFIG');
|
|
26
|
+
}
|
|
27
|
+
const frozen = rules.map((rule) => Object.freeze({ ...rule }));
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
detect(text) {
|
|
31
|
+
const findings = [];
|
|
32
|
+
detectNational(text, (finding) => findings.push(finding), frozen);
|
|
33
|
+
return findings;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Identity-document finding types, shared by the transforms and the
|
|
2
|
+
// identity detectors without loading the detectors themselves.
|
|
3
|
+
export const identityTypes = new Set([
|
|
4
|
+
'SSN',
|
|
5
|
+
'US_SSN',
|
|
6
|
+
'BRAZIL_CPF_NUMBER',
|
|
7
|
+
'INDIA_PAN_INDIVIDUAL',
|
|
8
|
+
'UK_NATIONAL_INSURANCE_NUMBER',
|
|
9
|
+
'SPAIN_DNI_NUMBER',
|
|
10
|
+
'US_HEALTHCARE_NPI',
|
|
11
|
+
'DRIVER_LICENSE',
|
|
12
|
+
'DRIVERS_LICENSE',
|
|
13
|
+
'NATIONAL_ID',
|
|
14
|
+
'CHINA_RESIDENT_ID_NUMBER',
|
|
15
|
+
'HONG_KONG_ID_NUMBER',
|
|
16
|
+
'SOUTH_AFRICA_ID_NUMBER',
|
|
17
|
+
'TAIWAN_ID_NUMBER',
|
|
18
|
+
'TURKEY_ID_NUMBER',
|
|
19
|
+
'GOVERNMENT_ID',
|
|
20
|
+
'DOD_ID_NUMBER',
|
|
21
|
+
'JAPAN_INDIVIDUAL_NUMBER',
|
|
22
|
+
'INDIA_GST_INDIVIDUAL',
|
|
23
|
+
'US_MEDICARE_BENEFICIARY_ID_NUMBER',
|
|
24
|
+
'ID',
|
|
25
|
+
'PASSPORT_NUMBER',
|
|
26
|
+
'PASSPORT_MRZ',
|
|
27
|
+
'BOOKING_REFERENCE',
|
|
28
|
+
'TICKET_NUMBER',
|
|
29
|
+
'VISA_NUMBER',
|
|
30
|
+
'UK_NHS',
|
|
31
|
+
'KR_RRN',
|
|
32
|
+
'IT_FISCAL_CODE',
|
|
33
|
+
'SG_NRIC_FIN',
|
|
34
|
+
'PL_PESEL',
|
|
35
|
+
'SE_PERSONNUMMER',
|
|
36
|
+
'ZA_ID_NUMBER',
|
|
37
|
+
'AU_TFN',
|
|
38
|
+
'CA_SIN',
|
|
39
|
+
'RU_SNILS',
|
|
40
|
+
'RU_INN',
|
|
41
|
+
'US_ITIN',
|
|
42
|
+
'US_DRIVER_LICENSE',
|
|
43
|
+
'UK_DRIVING_LICENCE',
|
|
44
|
+
'ES_NIE',
|
|
45
|
+
'IN_AADHAAR',
|
|
46
|
+
'IN_VOTER',
|
|
47
|
+
'IN_PASSPORT',
|
|
48
|
+
'FI_PERSONAL_IDENTITY_CODE',
|
|
49
|
+
'TH_TNIN',
|
|
50
|
+
'AU_MEDICARE',
|
|
51
|
+
'DE_TAX_ID',
|
|
52
|
+
'AE_EMIRATES_ID',
|
|
53
|
+
'SA_NATIONAL_ID',
|
|
54
|
+
'IL_ID_NUMBER',
|
|
55
|
+
]);
|
|
56
|
+
export function isIdentityType(type) {
|
|
57
|
+
return (
|
|
58
|
+
identityTypes.has(type) ||
|
|
59
|
+
/PASSPORT|(?:DRIVERS?|DRIVING)_?LICENSE|IDENTITY|NATIONAL_?(?:ID|IDENTIFICATION)|PERSONAL_?(?:ID|IDENTIFICATION)|UNIQUE_?IDENTIFICATION|SOCIAL_?(?:SECURITY|INSURANCE|IDENTIFICATION)|RESIDENT_?REGISTRATION|(?:TAX|TAXPAYER).*?(?:IDENTIFICATION|NUMBER|REFERENCE)|AADHAAR|FISCAL_CODE|PESEL|RRN|NHS|SNILS|INN_NUMBER/.test(
|
|
60
|
+
type
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
package/src/identity.js
CHANGED
|
@@ -1,52 +1,8 @@
|
|
|
1
1
|
import { collectMatches } from './detection.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'BRAZIL_CPF_NUMBER',
|
|
7
|
-
'INDIA_PAN_INDIVIDUAL',
|
|
8
|
-
'UK_NATIONAL_INSURANCE_NUMBER',
|
|
9
|
-
'SPAIN_DNI_NUMBER',
|
|
10
|
-
'US_HEALTHCARE_NPI',
|
|
11
|
-
'DRIVER_LICENSE',
|
|
12
|
-
'DRIVERS_LICENSE',
|
|
13
|
-
'NATIONAL_ID',
|
|
14
|
-
'CHINA_RESIDENT_ID_NUMBER',
|
|
15
|
-
'HONG_KONG_ID_NUMBER',
|
|
16
|
-
'SOUTH_AFRICA_ID_NUMBER',
|
|
17
|
-
'TAIWAN_ID_NUMBER',
|
|
18
|
-
'TURKEY_ID_NUMBER',
|
|
19
|
-
'GOVERNMENT_ID',
|
|
20
|
-
'DOD_ID_NUMBER',
|
|
21
|
-
'JAPAN_INDIVIDUAL_NUMBER',
|
|
22
|
-
'INDIA_GST_INDIVIDUAL',
|
|
23
|
-
'US_MEDICARE_BENEFICIARY_ID_NUMBER',
|
|
24
|
-
'ID',
|
|
25
|
-
'PASSPORT_NUMBER',
|
|
26
|
-
'PASSPORT_MRZ',
|
|
27
|
-
'BOOKING_REFERENCE',
|
|
28
|
-
'TICKET_NUMBER',
|
|
29
|
-
'VISA_NUMBER',
|
|
30
|
-
'UK_NHS',
|
|
31
|
-
'KR_RRN',
|
|
32
|
-
'IT_FISCAL_CODE',
|
|
33
|
-
'SG_NRIC_FIN',
|
|
34
|
-
'PL_PESEL',
|
|
35
|
-
'SE_PERSONNUMMER',
|
|
36
|
-
'ZA_ID_NUMBER',
|
|
37
|
-
'AU_TFN',
|
|
38
|
-
'CA_SIN',
|
|
39
|
-
'RU_SNILS',
|
|
40
|
-
'RU_INN',
|
|
41
|
-
]);
|
|
42
|
-
export function isIdentityType(type) {
|
|
43
|
-
return (
|
|
44
|
-
identityTypes.has(type) ||
|
|
45
|
-
/PASSPORT|(?:DRIVERS?|DRIVING)_?LICENSE|IDENTITY|NATIONAL_?(?:ID|IDENTIFICATION)|PERSONAL_?(?:ID|IDENTIFICATION)|UNIQUE_?IDENTIFICATION|SOCIAL_?(?:SECURITY|INSURANCE|IDENTIFICATION)|RESIDENT_?REGISTRATION|(?:TAX|TAXPAYER).*?(?:IDENTIFICATION|NUMBER|REFERENCE)|AADHAAR|FISCAL_CODE|PESEL|RRN|NHS|SNILS|INN_NUMBER/.test(
|
|
46
|
-
type
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
3
|
+
import { isIdentityType } from './identity-types.js';
|
|
4
|
+
|
|
5
|
+
export { identityTypes, isIdentityType } from './identity-types.js';
|
|
50
6
|
const personal = (type, rule, confidence = 0.85) => ({
|
|
51
7
|
type,
|
|
52
8
|
rule,
|
package/src/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface Finding {
|
|
|
17
17
|
kept?: 'fake';
|
|
18
18
|
/** Returned finding replaced with, or recognized as, a generated fake. */
|
|
19
19
|
faked?: true;
|
|
20
|
+
/** For `JSON_VALUE`: the field's personal type, e.g. `AGE`, when known. */
|
|
21
|
+
valueType?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface PersonalValue {
|
|
22
24
|
type: string;
|
|
@@ -31,7 +33,9 @@ export type Vocabulary = Partial<
|
|
|
31
33
|
| 'organizationCues'
|
|
32
34
|
| 'educationCues'
|
|
33
35
|
| 'personalLabels'
|
|
34
|
-
| 'personalFields'
|
|
36
|
+
| 'personalFields'
|
|
37
|
+
/** Word stems for the strict special-category rule; needs `strict: true`. */
|
|
38
|
+
| 'personalAttributes',
|
|
35
39
|
VocabularyList
|
|
36
40
|
>
|
|
37
41
|
>;
|
|
@@ -100,6 +104,33 @@ export interface SanitizerOptions {
|
|
|
100
104
|
minConfidence?: number;
|
|
101
105
|
/** Alias for minConfidence; specifying both requires equal values. */
|
|
102
106
|
threshold?: number;
|
|
107
|
+
/** Per-type thresholds (e.g. `{ PERSON: 0.8 }`) that override minConfidence. */
|
|
108
|
+
minConfidenceByType?: Record<string, number>;
|
|
109
|
+
/**
|
|
110
|
+
* Values kept as-is: a string must equal the whole detected value and a
|
|
111
|
+
* RegExp must fully match it. Declared knownPersonal/knownSecrets values and
|
|
112
|
+
* credentials (unless allowCredentialDisable) are never allow-listed.
|
|
113
|
+
*/
|
|
114
|
+
allowList?: (string | RegExp)[];
|
|
115
|
+
/**
|
|
116
|
+
* Honour scanner suppression comments (`gitleaks:allow`, `pragma: allowlist
|
|
117
|
+
* secret`, `pragma: allowlist nextline secret`, `NOSONAR`, ...) under the
|
|
118
|
+
* same rules as allowList.
|
|
119
|
+
*/
|
|
120
|
+
inlineAllow?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Keeps a finding the caller's policy knows is not sensitive. Runs after
|
|
123
|
+
* detection and before transformation, once per finding, with the text the
|
|
124
|
+
* offsets refer to; it must return a boolean (`true` keeps the span as-is).
|
|
125
|
+
* Verification of the output consults it again per span, so the same value
|
|
126
|
+
* elsewhere is still caught. Declared knownSecrets/knownPersonal/
|
|
127
|
+
* confirmedPersonal values and JSON_VALUE denials are never offered.
|
|
128
|
+
* createSyncSanitizer and sanitize() require a synchronous hook.
|
|
129
|
+
*/
|
|
130
|
+
exempt?: (
|
|
131
|
+
finding: Readonly<Finding & { value: string }>,
|
|
132
|
+
text: string
|
|
133
|
+
) => boolean | Promise<boolean>;
|
|
103
134
|
/** Explicit network/callback verification; credentials never qualify. */
|
|
104
135
|
verifyPublic?: (candidate: {
|
|
105
136
|
type: PublicEntity['type'];
|
|
@@ -122,6 +153,7 @@ export type DebugEvent =
|
|
|
122
153
|
| { event: 'detected'; count: number }
|
|
123
154
|
| { event: 'document-experience' | 'proper-name-prose'; count: number }
|
|
124
155
|
| { event: 'residual-redaction'; round: number }
|
|
156
|
+
| { event: 'template-value'; rule: string }
|
|
125
157
|
| { event: 'two-pass-confirmation'; type: string }
|
|
126
158
|
| { event: 'propagation-skipped'; reason: 'length'; type: string }
|
|
127
159
|
| { event: 'gitleaks-rule'; rule: string; length: number; ms: number };
|
|
@@ -228,7 +260,11 @@ export declare function fromOffsetReport(
|
|
|
228
260
|
): Finding[];
|
|
229
261
|
export declare function knownSecretsFromEnv(
|
|
230
262
|
env?: Record<string, string | undefined>,
|
|
231
|
-
names?: string[]
|
|
263
|
+
names?: string[],
|
|
264
|
+
options?: {
|
|
265
|
+
/** Skip values shorter than this many characters (default 1). */
|
|
266
|
+
minLength?: number;
|
|
267
|
+
}
|
|
232
268
|
): string[];
|
|
233
269
|
/** Reversible UTF-8 view: invalid bytes use unpaired U+DC80..U+DCFF. */
|
|
234
270
|
export declare function decodeLogBytes(bytes: Uint8Array): string;
|
|
@@ -285,6 +321,12 @@ export type Transformation =
|
|
|
285
321
|
map?: RestoreMap | false;
|
|
286
322
|
format?: string;
|
|
287
323
|
credentials?: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* Tokens of the namespace already in the input: `'error'` (default)
|
|
326
|
+
* throws `ERR_PLACEHOLDER_COLLISION`; `'keep'` leaves them as they are
|
|
327
|
+
* and numbers new values after the highest kept number per type.
|
|
328
|
+
*/
|
|
329
|
+
existing?: 'error' | 'keep';
|
|
288
330
|
}
|
|
289
331
|
| {
|
|
290
332
|
mode: 'mask';
|
|
@@ -292,6 +334,17 @@ export type Transformation =
|
|
|
292
334
|
keepEnd?: number;
|
|
293
335
|
marker?: string;
|
|
294
336
|
minLength?: number;
|
|
337
|
+
/** One masking character, default `*`; not combinable with marker. */
|
|
338
|
+
char?: string;
|
|
339
|
+
/** Characters left unmasked, e.g. `'-'` for `****-****-1111`. */
|
|
340
|
+
ignore?: string;
|
|
341
|
+
ignoreClasses?: (
|
|
342
|
+
'digits' | 'upper' | 'lower' | 'punctuation' | 'whitespace'
|
|
343
|
+
)[];
|
|
344
|
+
/** Mask at most this many maskable characters. */
|
|
345
|
+
count?: number;
|
|
346
|
+
/** Apply `count` from the end instead of the start. */
|
|
347
|
+
fromEnd?: boolean;
|
|
295
348
|
}
|
|
296
349
|
| { mode: 'pseudonym' | 'format-preserving'; key: string }
|
|
297
350
|
| {
|
|
@@ -308,7 +361,9 @@ export type Transformation =
|
|
|
308
361
|
>;
|
|
309
362
|
}
|
|
310
363
|
| { mode: 'date-shift'; days: number }
|
|
311
|
-
| { mode: 'bucket'; size: number }
|
|
364
|
+
| { mode: 'bucket'; size: number }
|
|
365
|
+
/** Deterministic AES-256-GCM `[ENCRYPTED:TYPE:…]`; key >= 16 UTF-8 bytes. */
|
|
366
|
+
| { mode: 'encrypt'; key: string };
|
|
312
367
|
|
|
313
368
|
/** Contains original personal data; never publish this object or its files. */
|
|
314
369
|
export interface RestoreMap {
|
|
@@ -350,6 +405,18 @@ export declare function writeRestoreMap(
|
|
|
350
405
|
): Promise<void>;
|
|
351
406
|
/** Exact original-document restoration; derived text uses canonical surfaces. */
|
|
352
407
|
export declare function restore(text: string, map: RestoreMap): string;
|
|
408
|
+
/** Sorted, frozen list of the types built-in rules turn into `Type-n` tokens. */
|
|
409
|
+
export declare function reservedPlaceholderTypes(): readonly string[];
|
|
410
|
+
/** Whether `text` is one whole token of the placeholder namespace. */
|
|
411
|
+
export declare function isPlaceholderToken(
|
|
412
|
+
text: string,
|
|
413
|
+
options?: {
|
|
414
|
+
/** Placeholder format, default `'{Type}-{n}'`. */
|
|
415
|
+
format?: string;
|
|
416
|
+
/** Extra custom types, e.g. those of `createPatternDetector`. */
|
|
417
|
+
types?: readonly string[];
|
|
418
|
+
}
|
|
419
|
+
): boolean;
|
|
353
420
|
export interface StreamOptions extends SanitizerOptions {
|
|
354
421
|
/** Treat NUL as a log byte; default false. Incompatible with JSON/JSONL. */
|
|
355
422
|
allowBinary?: boolean;
|
|
@@ -392,6 +459,93 @@ export declare const entityCatalogs: Record<
|
|
|
392
459
|
'google' | 'azure' | 'aws',
|
|
393
460
|
{ source: string; sha256: string; names: string[] }
|
|
394
461
|
>;
|
|
462
|
+
/** Built-in identifier rule; `context` is tested near each `pattern` match. */
|
|
463
|
+
export interface IdentifierRule {
|
|
464
|
+
type: string;
|
|
465
|
+
/** Global regular expression for candidate values. */
|
|
466
|
+
pattern: RegExp;
|
|
467
|
+
/** Non-global label/context expression, 64 characters before to 32 after. */
|
|
468
|
+
context: RegExp;
|
|
469
|
+
/** Checksum or issuing-format check that raises confidence. */
|
|
470
|
+
checksum: (value: string) => boolean;
|
|
471
|
+
/** Distinctive shape that is reported without context. */
|
|
472
|
+
plausible?: (value: string) => boolean;
|
|
473
|
+
/** Mandatory gate; candidates failing it are never reported. */
|
|
474
|
+
valid?: (value: string) => boolean;
|
|
475
|
+
}
|
|
476
|
+
export declare const nationalRules: readonly IdentifierRule[];
|
|
477
|
+
/** Format and checksum validators; they never prove issuance. */
|
|
478
|
+
export declare const identifierChecks: Readonly<
|
|
479
|
+
Record<string, (value: string, ...args: number[]) => boolean>
|
|
480
|
+
> & {
|
|
481
|
+
digitsOf(value: string): string;
|
|
482
|
+
};
|
|
483
|
+
/** Wrap identifier rules as a Detector for `detectors` options. */
|
|
484
|
+
export declare function createIdentifierDetector(
|
|
485
|
+
rules: readonly IdentifierRule[],
|
|
486
|
+
options?: { id?: string }
|
|
487
|
+
): Detector;
|
|
488
|
+
/** Restore `[ENCRYPTED:TYPE:…]` tokens; throws ERR_MAP on a wrong key. */
|
|
489
|
+
export declare function decrypt(
|
|
490
|
+
text: string,
|
|
491
|
+
key: string,
|
|
492
|
+
options?: { json?: boolean }
|
|
493
|
+
): string;
|
|
494
|
+
export interface PatternSpec {
|
|
495
|
+
type: string;
|
|
496
|
+
category?: Category;
|
|
497
|
+
/** Finding rule id, default `custom-pattern`. */
|
|
498
|
+
rule?: string;
|
|
499
|
+
/** Default base confidence, 0.6. */
|
|
500
|
+
confidence?: number;
|
|
501
|
+
patterns?: (
|
|
502
|
+
| RegExp
|
|
503
|
+
| string
|
|
504
|
+
| { regex: RegExp | string; confidence?: number; group?: number }
|
|
505
|
+
)[];
|
|
506
|
+
/** Whole-word terms (case-insensitive unless caseSensitive). */
|
|
507
|
+
denyList?: string[];
|
|
508
|
+
caseSensitive?: boolean;
|
|
509
|
+
denyListConfidence?: number;
|
|
510
|
+
/** Hotwords that adjust confidence when found near a match. */
|
|
511
|
+
context?: string[];
|
|
512
|
+
/** Characters searched for context, default `{ before: 64, after: 16 }`. */
|
|
513
|
+
contextWindow?: number | { before?: number; after?: number };
|
|
514
|
+
/** Added when context is found (default 0.35); negative lowers it. */
|
|
515
|
+
contextBoost?: number;
|
|
516
|
+
/** false rejects the match; a number in 0..1 sets its confidence. */
|
|
517
|
+
validate?: (value: string) => boolean | number | undefined;
|
|
518
|
+
}
|
|
519
|
+
/** Declarative regex/deny-list recognizers (Presidio/Google custom infoType style). */
|
|
520
|
+
export declare function createPatternDetector(
|
|
521
|
+
specs: PatternSpec | readonly PatternSpec[],
|
|
522
|
+
options?: { id?: string }
|
|
523
|
+
): Detector;
|
|
524
|
+
export interface SarifLog {
|
|
525
|
+
$schema: string;
|
|
526
|
+
version: '2.1.0';
|
|
527
|
+
runs: unknown[];
|
|
528
|
+
}
|
|
529
|
+
/** SARIF 2.1.0 log of rules and locations; never contains matched values. */
|
|
530
|
+
export declare function toSarif(
|
|
531
|
+
report:
|
|
532
|
+
| {
|
|
533
|
+
complete?: boolean;
|
|
534
|
+
results: { path: string; findings: Finding[] }[];
|
|
535
|
+
skipped?: { path: string; reason: string }[];
|
|
536
|
+
}
|
|
537
|
+
| { path: string; findings: Finding[] }[]
|
|
538
|
+
| Finding[],
|
|
539
|
+
options?: { version?: string; path?: string }
|
|
540
|
+
): SarifLog;
|
|
541
|
+
/**
|
|
542
|
+
* Offline CRC32/base62 check of GitHub (gh[opsur]_) and npm (npm_) tokens:
|
|
543
|
+
* true when the checksum matches, false when it does not, undefined for
|
|
544
|
+
* other formats.
|
|
545
|
+
*/
|
|
546
|
+
export declare function tokenChecksum(value: string): boolean | undefined;
|
|
547
|
+
/** Builds a checksum-valid token from a prefix ending in `_` and a 30-character body. */
|
|
548
|
+
export declare function tokenWithChecksum(prefix: string, body: string): string;
|
|
395
549
|
export declare function createWikidataVerifier(options?: {
|
|
396
550
|
fetch?: typeof fetch;
|
|
397
551
|
language?: string;
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Sensitive text detection and verified publication APIs. */
|
|
2
2
|
|
|
3
|
+
import './personal-features.js';
|
|
4
|
+
|
|
3
5
|
export { inspect, sanitize, redact } from './sanitizer.js';
|
|
4
6
|
export { createSanitizer } from './engines.js';
|
|
5
7
|
export { compileKnown } from './compiled-known.js';
|
|
@@ -26,6 +28,13 @@ export {
|
|
|
26
28
|
} from './external.js';
|
|
27
29
|
|
|
28
30
|
export { entityCatalogs } from './entities.js';
|
|
31
|
+
export { nationalRules } from './national.js';
|
|
32
|
+
export * as identifierChecks from './national-checks.js';
|
|
33
|
+
export { createIdentifierDetector } from './identifier-detector.js';
|
|
34
|
+
export { decrypt } from './encrypt.js';
|
|
35
|
+
export { createPatternDetector } from './pattern-detector.js';
|
|
36
|
+
export { toSarif } from './sarif.js';
|
|
37
|
+
export { tokenChecksum, tokenWithChecksum } from './token-checksum.js';
|
|
29
38
|
export {
|
|
30
39
|
sanitizeStream,
|
|
31
40
|
sanitizeStreamToFile,
|
|
@@ -51,3 +60,7 @@ export {
|
|
|
51
60
|
readRestoreMap,
|
|
52
61
|
writeRestoreMap,
|
|
53
62
|
} from './restore-map.js';
|
|
63
|
+
export {
|
|
64
|
+
reservedPlaceholderTypes,
|
|
65
|
+
isPlaceholderToken,
|
|
66
|
+
} from './placeholder-types.js';
|
package/src/known.js
CHANGED
|
@@ -71,28 +71,44 @@ function configTokens(config) {
|
|
|
71
71
|
return [...values];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Credential keywords between `_` boundaries, plus suffixes that are written
|
|
75
|
+
// without one (`PGPASSWORD`, `GITHUBTOKEN`, `SESSIONCOOKIE`).
|
|
74
76
|
const secretEnvName =
|
|
75
|
-
/(?:^|_)(?:TOKEN|(?:API|ACCESS|PRIVATE)?KEY|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH|PAT)(?:_|$)
|
|
76
|
-
// Configuration suffixes describe how authentication works
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/(?:^|_)(?:TOKEN|(?:API|ACCESS|PRIVATE)?KEY|SECRET|PASSWORD|PASSWD|CREDENTIALS?|O?AUTH|PAT|COOKIE)(?:_|$)|(?:PASSWORD|PASSWD|TOKEN|COOKIE)$/i;
|
|
78
|
+
// Configuration suffixes describe a credential (how authentication works,
|
|
79
|
+
// counters, limits, locations, names, identifiers and guard flags), not the
|
|
80
|
+
// credential itself. Explicit names bypass this filter; credential suffixes
|
|
81
|
+
// such as AUTH_MODE_TOKEN still win.
|
|
82
|
+
const configurationName =
|
|
83
|
+
/_(?:TRANSPORT|MODE|METHOD|TYPE|URL|URI|HOST|HOSTNAME|PATH|ENABLED|DISABLED|PORT|PROTOCOL|SCHEME|GUARD|COUNT|LIMIT|FILE|FILENAME|DIR|DIRECTORY|NAME|ID)$|^GIT_CONFIG_KEY_\d+$|^GOOGLE_APPLICATION_CREDENTIALS$/i;
|
|
84
|
+
// `GIT_CONFIG_KEY_<n>` names a git setting; `GIT_CONFIG_VALUE_<n>` holds the
|
|
85
|
+
// value, which is a credential for headers, passwords and tokens.
|
|
86
|
+
const gitCredentialSetting =
|
|
87
|
+
/(?:^|\.)(?:extraheader|password|token|secret|cookiefile|auth[a-z]*)$/i;
|
|
88
|
+
|
|
89
|
+
function discoveredNames(env) {
|
|
90
|
+
return Object.keys(env).filter((name) => {
|
|
91
|
+
const git = name.match(/^GIT_CONFIG_VALUE_(\d+)$/);
|
|
92
|
+
if (git) {
|
|
93
|
+
return gitCredentialSetting.test(env[`GIT_CONFIG_KEY_${git[1]}`] ?? '');
|
|
94
|
+
}
|
|
95
|
+
return secretEnvName.test(name) && !configurationName.test(name);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
/** Opt-in literal values from credential-named keys, or an explicit selection. */
|
|
81
|
-
export function knownSecretsFromEnv(env = process.env, names) {
|
|
100
|
+
export function knownSecretsFromEnv(env = process.env, names, options = {}) {
|
|
101
|
+
const { minLength = 1 } = options ?? {};
|
|
82
102
|
if (names === undefined) {
|
|
83
|
-
names =
|
|
84
|
-
env && typeof env === 'object'
|
|
85
|
-
? Object.keys(env).filter(
|
|
86
|
-
(name) =>
|
|
87
|
-
secretEnvName.test(name) && !authConfigurationName.test(name)
|
|
88
|
-
)
|
|
89
|
-
: [];
|
|
103
|
+
names = env && typeof env === 'object' ? discoveredNames(env) : [];
|
|
90
104
|
}
|
|
91
105
|
if (
|
|
92
106
|
!env ||
|
|
93
107
|
typeof env !== 'object' ||
|
|
94
108
|
!Array.isArray(names) ||
|
|
95
|
-
names.some((name) => typeof name !== 'string')
|
|
109
|
+
names.some((name) => typeof name !== 'string') ||
|
|
110
|
+
!Number.isSafeInteger(minLength) ||
|
|
111
|
+
minLength < 1
|
|
96
112
|
) {
|
|
97
113
|
throw failure('ERR_CONFIG');
|
|
98
114
|
}
|
|
@@ -100,7 +116,9 @@ export function knownSecretsFromEnv(env = process.env, names) {
|
|
|
100
116
|
...new Set(
|
|
101
117
|
names
|
|
102
118
|
.map((name) => env[name])
|
|
103
|
-
.filter(
|
|
119
|
+
.filter(
|
|
120
|
+
(value) => typeof value === 'string' && value.length >= minLength
|
|
121
|
+
)
|
|
104
122
|
),
|
|
105
123
|
];
|
|
106
124
|
}
|