@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { failure, escapePattern } from './detection.js';
|
|
2
|
+
|
|
3
|
+
// Declarative recognizers in the style of Presidio's PatternRecognizer
|
|
4
|
+
// (patterns with scores, deny lists, context words, validate_result) and
|
|
5
|
+
// Google custom infoTypes (regex/dictionary plus hotword likelihood rules).
|
|
6
|
+
// The result is an ordinary Detector, so it composes with every other port.
|
|
7
|
+
const typePattern = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
8
|
+
const rulePattern = /^[a-z0-9@/._-]{1,120}$/;
|
|
9
|
+
const maxPatterns = 64,
|
|
10
|
+
maxWords = 10000;
|
|
11
|
+
|
|
12
|
+
const score = (value) =>
|
|
13
|
+
typeof value === 'number' &&
|
|
14
|
+
Number.isFinite(value) &&
|
|
15
|
+
value >= 0 &&
|
|
16
|
+
value <= 1;
|
|
17
|
+
const optional = (value, test) => value === undefined || test(value);
|
|
18
|
+
const words = (list) =>
|
|
19
|
+
Array.isArray(list) &&
|
|
20
|
+
list.length <= maxWords &&
|
|
21
|
+
list.every((word) => typeof word === 'string' && word.trim().length > 0);
|
|
22
|
+
|
|
23
|
+
const validEntry = (raw) =>
|
|
24
|
+
Boolean(raw) &&
|
|
25
|
+
(raw.regex instanceof RegExp ||
|
|
26
|
+
(typeof raw.regex === 'string' && raw.regex.length > 0)) &&
|
|
27
|
+
optional(raw.confidence, score) &&
|
|
28
|
+
optional(raw.group, (group) => Number.isSafeInteger(group) && group >= 0);
|
|
29
|
+
|
|
30
|
+
function globalRegex(regex) {
|
|
31
|
+
try {
|
|
32
|
+
return regex instanceof RegExp
|
|
33
|
+
? new RegExp(
|
|
34
|
+
regex.source,
|
|
35
|
+
regex.flags.includes('g') ? regex.flags : `${regex.flags}g`
|
|
36
|
+
)
|
|
37
|
+
: new RegExp(regex, 'gu');
|
|
38
|
+
} catch {
|
|
39
|
+
throw failure('ERR_CONFIG');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compile(entry) {
|
|
44
|
+
const raw =
|
|
45
|
+
entry instanceof RegExp || typeof entry === 'string'
|
|
46
|
+
? { regex: entry }
|
|
47
|
+
: entry;
|
|
48
|
+
if (!validEntry(raw)) {
|
|
49
|
+
throw failure('ERR_CONFIG');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
regex: globalRegex(raw.regex),
|
|
53
|
+
confidence: raw.confidence,
|
|
54
|
+
group: raw.group ?? 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function window(value) {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return { before: 64, after: 16 };
|
|
61
|
+
}
|
|
62
|
+
const shape = typeof value === 'number' ? { before: value, after: 0 } : value;
|
|
63
|
+
if (
|
|
64
|
+
!shape ||
|
|
65
|
+
![shape.before ?? 0, shape.after ?? 0].every(
|
|
66
|
+
(n) => Number.isSafeInteger(n) && n >= 0 && n <= 4096
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
throw failure('ERR_CONFIG');
|
|
70
|
+
}
|
|
71
|
+
return { before: shape.before ?? 0, after: shape.after ?? 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const specChecks = [
|
|
75
|
+
(spec) => typePattern.test(spec.type ?? ''),
|
|
76
|
+
(spec) =>
|
|
77
|
+
optional(spec.category, (c) => ['personal', 'credential'].includes(c)),
|
|
78
|
+
(spec) => optional(spec.rule, (rule) => rulePattern.test(rule)),
|
|
79
|
+
(spec) => optional(spec.confidence, score),
|
|
80
|
+
(spec) => optional(spec.denyListConfidence, score),
|
|
81
|
+
(spec) =>
|
|
82
|
+
optional(
|
|
83
|
+
spec.patterns,
|
|
84
|
+
(list) => Array.isArray(list) && list.length <= maxPatterns
|
|
85
|
+
),
|
|
86
|
+
(spec) => optional(spec.denyList, words),
|
|
87
|
+
(spec) => optional(spec.context, words),
|
|
88
|
+
(spec) =>
|
|
89
|
+
optional(
|
|
90
|
+
spec.contextBoost,
|
|
91
|
+
(boost) => typeof boost === 'number' && Math.abs(boost) <= 1
|
|
92
|
+
),
|
|
93
|
+
(spec) => optional(spec.validate, (fn) => typeof fn === 'function'),
|
|
94
|
+
(spec) => Boolean(spec.patterns?.length || spec.denyList?.length),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
function compileSpec(spec) {
|
|
98
|
+
if (
|
|
99
|
+
!spec ||
|
|
100
|
+
typeof spec !== 'object' ||
|
|
101
|
+
!specChecks.every((check) => check(spec))
|
|
102
|
+
) {
|
|
103
|
+
throw failure('ERR_CONFIG');
|
|
104
|
+
}
|
|
105
|
+
const patterns = (spec.patterns ?? []).map(compile);
|
|
106
|
+
if (spec.denyList?.length) {
|
|
107
|
+
patterns.push({
|
|
108
|
+
regex: new RegExp(
|
|
109
|
+
`(?<![\\p{L}\\p{N}_])(?:${[...spec.denyList]
|
|
110
|
+
.sort((a, b) => b.length - a.length)
|
|
111
|
+
.map(escapePattern)
|
|
112
|
+
.join('|')})(?![\\p{L}\\p{N}_])`,
|
|
113
|
+
spec.caseSensitive ? 'gu' : 'giu'
|
|
114
|
+
),
|
|
115
|
+
confidence: spec.denyListConfidence ?? 1,
|
|
116
|
+
group: 0,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const context = spec.context?.length
|
|
120
|
+
? new RegExp(
|
|
121
|
+
`(?<![\\p{L}\\p{N}])(?:${spec.context.map(escapePattern).join('|')})(?![\\p{L}\\p{N}])`,
|
|
122
|
+
'iu'
|
|
123
|
+
)
|
|
124
|
+
: undefined;
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
type: spec.type,
|
|
127
|
+
category: spec.category ?? 'personal',
|
|
128
|
+
rule: spec.rule ?? 'custom-pattern',
|
|
129
|
+
confidence: spec.confidence ?? 0.6,
|
|
130
|
+
patterns,
|
|
131
|
+
context,
|
|
132
|
+
window: window(spec.contextWindow),
|
|
133
|
+
boost: spec.contextBoost ?? 0.35,
|
|
134
|
+
validate: spec.validate,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scored(spec, base, text, start, end, value) {
|
|
139
|
+
let confidence = base;
|
|
140
|
+
if (
|
|
141
|
+
spec.context?.test(
|
|
142
|
+
`${text.slice(Math.max(0, start - spec.window.before), start)}\n${text.slice(end, end + spec.window.after)}`
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
// Presidio raises a context-supported score by 0.35 with a 0.4 floor;
|
|
146
|
+
// a negative boost models Google's "decrease likelihood" hotwords.
|
|
147
|
+
confidence =
|
|
148
|
+
spec.boost >= 0
|
|
149
|
+
? Math.max(0.4, confidence + spec.boost)
|
|
150
|
+
: confidence + spec.boost;
|
|
151
|
+
}
|
|
152
|
+
if (spec.validate) {
|
|
153
|
+
const verdict = spec.validate(value);
|
|
154
|
+
if (verdict === false) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
if (typeof verdict === 'number' && score(verdict)) {
|
|
158
|
+
confidence = verdict;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Math.min(1, Math.max(0, confidence));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function* matches(spec, text) {
|
|
165
|
+
for (const pattern of spec.patterns) {
|
|
166
|
+
pattern.regex.lastIndex = 0;
|
|
167
|
+
for (const match of text.matchAll(pattern.regex)) {
|
|
168
|
+
const value = match[pattern.group];
|
|
169
|
+
if (!value) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const start = match.index + (pattern.group ? match[0].indexOf(value) : 0);
|
|
173
|
+
yield {
|
|
174
|
+
start,
|
|
175
|
+
end: start + value.length,
|
|
176
|
+
value,
|
|
177
|
+
base: pattern.confidence ?? spec.confidence,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds a synchronous Detector from declarative specs. Each spec matches
|
|
185
|
+
* `patterns` (RegExp, string sources or `{regex, confidence, group}`) and/or
|
|
186
|
+
* a whole-word `denyList`, raises or lowers confidence when a `context` word
|
|
187
|
+
* occurs within `contextWindow`, and lets `validate(value)` reject a match
|
|
188
|
+
* (false) or set its confidence (0–1). Patterns are trusted configuration:
|
|
189
|
+
* avoid catastrophic backtracking in caller-supplied expressions.
|
|
190
|
+
*/
|
|
191
|
+
export function createPatternDetector(specs, { id = 'pattern-rules' } = {}) {
|
|
192
|
+
const list = Array.isArray(specs) ? specs : [specs];
|
|
193
|
+
if (!list.length || list.length > maxPatterns) {
|
|
194
|
+
throw failure('ERR_CONFIG');
|
|
195
|
+
}
|
|
196
|
+
const compiled = list.map(compileSpec);
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
detect(text) {
|
|
200
|
+
const findings = [];
|
|
201
|
+
for (const spec of compiled) {
|
|
202
|
+
for (const { start, end, value, base } of matches(spec, text)) {
|
|
203
|
+
const confidence = scored(spec, base, text, start, end, value);
|
|
204
|
+
if (confidence !== undefined) {
|
|
205
|
+
findings.push({
|
|
206
|
+
start,
|
|
207
|
+
end,
|
|
208
|
+
type: spec.type,
|
|
209
|
+
category: spec.category,
|
|
210
|
+
rule: spec.rule,
|
|
211
|
+
confidence,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return findings;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/personal-context.js
CHANGED
|
@@ -6,8 +6,9 @@ export const months =
|
|
|
6
6
|
'January February March April May June July August September October November December Jan Feb Mar Apr Jun Jul Aug Sep Sept Oct Nov Dec января февраля марта апреля мая июня июля августа сентября октября ноября декабря январь февраль март апрель май июнь июль август сентябрь октябрь ноябрь декабрь'
|
|
7
7
|
.split(' ')
|
|
8
8
|
.join('|');
|
|
9
|
+
const ordinal = '(?:st|nd|rd|th)?';
|
|
9
10
|
const dates = new RegExp(
|
|
10
|
-
`(?<![\\p{L}\\p{N}_])(?:\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[
|
|
11
|
+
`(?<![\\p{L}\\p{N}_])(?:\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[/.-]\\d{1,2}[/.-]\\d{4}|\\d{1,2}${ordinal}[ \\t]+(?:of[ \\t]+)?(?:${months})\\.?,?[ \\t]+\\d{4}|(?:${months})\\.?[ \\t]+\\d{1,2}${ordinal},?[ \\t]+\\d{4})(?![\\p{L}\\p{N}_])`,
|
|
11
12
|
'giu'
|
|
12
13
|
);
|
|
13
14
|
const phoneContext =
|
|
@@ -30,7 +31,7 @@ export function detectPersonalContext(text, emit) {
|
|
|
30
31
|
);
|
|
31
32
|
collectMatches(
|
|
32
33
|
text,
|
|
33
|
-
/(?<![\p{L}\p{N}_])(?:phone|fax|ĐT|điện thoại)(?:[ \t]*[::][ \t]*|[ \t]+)(\+?\(?\d[\d() .-]{5,38}\d)(?![\p{L}\p{N}_])/giu,
|
|
34
|
+
/(?<![\p{L}\p{N}_])(?:(?:phone|fax|ĐT|điện thoại)(?:[ \t]*[::][ \t]*|[ \t]+)|(?:telephone|tel|mobile|mob|cell|desk|ph)\.?[ \t]*[::][ \t]*)(\+?\(?\d[\d() .-]{5,38}\d)(?![\p{L}\p{N}_])/giu,
|
|
34
35
|
emit,
|
|
35
36
|
phone,
|
|
36
37
|
1,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { collectMatches } from './detection.js';
|
|
2
|
+
import { credentialPlaceholder } from './credentials.js';
|
|
3
|
+
|
|
4
|
+
// Phrase-level evidence collected from competitor test suites (#128):
|
|
5
|
+
// self-introductions, letter salutations and sign-offs, mail display names,
|
|
6
|
+
// spelled-out e-mail addresses, post-office boxes, labelled messenger
|
|
7
|
+
// handles, username/password pairs and labelled card numbers.
|
|
8
|
+
const personal = (type, rule, confidence = 0.85) => ({
|
|
9
|
+
type,
|
|
10
|
+
rule,
|
|
11
|
+
category: 'personal',
|
|
12
|
+
confidence,
|
|
13
|
+
});
|
|
14
|
+
const word = "\\p{Lu}[\\p{Ll}\\p{M}'’]+(?:\\p{Lu}[\\p{Ll}\\p{M}]+)?";
|
|
15
|
+
const nameWords = `${word}(?:-${word})?(?:[ \\t]+(?:${word}(?:-${word})?|\\p{Lu}\\.)){0,2}`;
|
|
16
|
+
// Addressees and senders that are roles, not people.
|
|
17
|
+
const roleWord =
|
|
18
|
+
/^(?:Sir|Madam|Madame|Sirs|All|Everyone|Everybody|Team|Folks|Friends?|Colleagues?|Customer|Client|User|Users|Members?|Support|Admin|Team[ \t].*|Hiring[ \t]+Manager|Recruiter|There|Guys|Again|You|Me|Us|Them|God|Mom|Dad|Sorry|Thanks|Yes|No|Not|Here|Also|Just|Back|Done|OK|Okay|The|Our|Your|My|This|World)$/iu;
|
|
19
|
+
const person = (value) =>
|
|
20
|
+
!roleWord.test(value) && !roleWord.test(value.split(/[ \t]+/)[0]);
|
|
21
|
+
|
|
22
|
+
const dash = '(?:-{1,2}|—|–|\\.{2,3}|~)';
|
|
23
|
+
function detectNames(text, emit) {
|
|
24
|
+
// "My name is Bond", "I'm called Jane Doe"
|
|
25
|
+
collectMatches(
|
|
26
|
+
text,
|
|
27
|
+
new RegExp(
|
|
28
|
+
`(?<![\\p{L}\\p{N}_])(?:[Mm]y[ \\t]+name[ \\t]+is|[Nn]ame's|I[ \\t]+am[ \\t]+called|[Cc]all[ \\t]+me)[ \\t]+(${nameWords})(?![\\p{L}\\p{M}])`,
|
|
29
|
+
'gu'
|
|
30
|
+
),
|
|
31
|
+
emit,
|
|
32
|
+
personal('PERSON', 'self-introduction', 0.9),
|
|
33
|
+
1,
|
|
34
|
+
person
|
|
35
|
+
);
|
|
36
|
+
// "Dear Clifford," and "Hi Jane Doe," at the start of a line.
|
|
37
|
+
collectMatches(
|
|
38
|
+
text,
|
|
39
|
+
new RegExp(
|
|
40
|
+
`(?:^|(?<=[\\r\\n]))[ \\t]*(?:Dear|Hi|Hello|Hey|Greetings)[ \\t]+(${nameWords})[ \\t]*[,!:]`,
|
|
41
|
+
'gmu'
|
|
42
|
+
),
|
|
43
|
+
emit,
|
|
44
|
+
personal('PERSON', 'salutation', 0.85),
|
|
45
|
+
1,
|
|
46
|
+
person
|
|
47
|
+
);
|
|
48
|
+
// "Thanks -Jon", "All the best,\n\n--Meg C.", "Thank you ..Risemamy McCrubben"
|
|
49
|
+
collectMatches(
|
|
50
|
+
text,
|
|
51
|
+
new RegExp(
|
|
52
|
+
`(?<![\\p{L}\\p{N}_])(?:Thanks|Thank[ \\t]+you|Many[ \\t]+thanks|Regards|Best[ \\t]+regards|Kind[ \\t]+regards|Warm[ \\t]+regards|Best[ \\t]+wishes|All[ \\t]+the[ \\t]+best|Cheers|Sincerely|Yours[ \\t]+(?:truly|sincerely)|Best)(?:[ \\t]*[,!.]?[ \\t]*(?:\\r?\\n[ \\t]*){1,3}${dash}?|[ \\t]*[,!.][ \\t]*${dash}?|[ \\t]*${dash})[ \\t]*(${nameWords})[ \\t]*\\.?[ \\t]*(?=\\r?\\n|$)`,
|
|
53
|
+
'gu'
|
|
54
|
+
),
|
|
55
|
+
emit,
|
|
56
|
+
personal('PERSON', 'sign-off', 0.85),
|
|
57
|
+
1,
|
|
58
|
+
person
|
|
59
|
+
);
|
|
60
|
+
// Mail display names: `Bob Stone <bob.stone@example.com>`.
|
|
61
|
+
collectMatches(
|
|
62
|
+
text,
|
|
63
|
+
new RegExp(
|
|
64
|
+
`(?<![\\p{L}\\p{N}_])"?(${word}(?:[ \\t]+(?:${word}|\\p{Lu}\\.)){1,3})"?[ \\t]*<[^\\s<>@]+@[^\\s<>@]+>`,
|
|
65
|
+
'gu'
|
|
66
|
+
),
|
|
67
|
+
emit,
|
|
68
|
+
personal('PERSON', 'mail-display-name', 0.9),
|
|
69
|
+
1,
|
|
70
|
+
person
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const freemail =
|
|
75
|
+
'gmail|googlemail|yahoo|ymail|hotmail|outlook|live|msn|icloud|me|mac|aol|proton|protonmail|pm|gmx|mail|yandex|zoho|fastmail|hey|tutanota|qq|163|126|naver|web';
|
|
76
|
+
const at =
|
|
77
|
+
'(?:[ \\t]*[[({<][ \\t]*at[ \\t]*[\\])}>][ \\t]*|[ \\t]+at[ \\t]+|@)';
|
|
78
|
+
const dot =
|
|
79
|
+
'(?:[ \\t]*[[({<][ \\t]*dot[ \\t]*[\\])}>][ \\t]*|[ \\t]+dot[ \\t]+|\\.)';
|
|
80
|
+
const obfuscatedEmail = new RegExp(
|
|
81
|
+
`(?<![\\p{L}\\p{N}_.+-])[\\p{L}\\p{N}_+-]+(?:${dot}[\\p{L}\\p{N}_+-]+)*${at}[\\p{L}\\p{N}-]+(?:${dot}[\\p{L}\\p{N}-]+)*${dot}\\p{L}{2,24}(?![\\p{L}\\p{N}_-])`,
|
|
82
|
+
'giu'
|
|
83
|
+
);
|
|
84
|
+
// Spelled-out separators are evidence; a plain " at " needs a well-known
|
|
85
|
+
// mailbox provider so that "meet at example.com" stays prose.
|
|
86
|
+
function spelledEmail(value) {
|
|
87
|
+
const bracketed = /[[({<][ \t]*(?:at|dot)[ \t]*[\])}>]/i.test(value);
|
|
88
|
+
const spelled = /[ \t]dot[ \t]/i.test(value);
|
|
89
|
+
const plainAt = new RegExp(
|
|
90
|
+
`[ \\t]at[ \\t]+(?:${freemail})\\.[a-z]{2,6}(?:\\.[a-z]{2})?$`,
|
|
91
|
+
'i'
|
|
92
|
+
).test(value);
|
|
93
|
+
return (
|
|
94
|
+
!/^[^@]+@[^@\s[({<]+$/.test(value) && (bracketed || spelled || plainAt)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function detectContactForms(text, emit) {
|
|
99
|
+
collectMatches(
|
|
100
|
+
text,
|
|
101
|
+
obfuscatedEmail,
|
|
102
|
+
emit,
|
|
103
|
+
personal('EMAIL', 'obfuscated-email', 0.85),
|
|
104
|
+
0,
|
|
105
|
+
spelledEmail
|
|
106
|
+
);
|
|
107
|
+
collectMatches(
|
|
108
|
+
text,
|
|
109
|
+
/(?<![\p{L}\p{N}_])(?:P\.?[ \t]?O\.?[ \t]?Box|Post[ \t]+Office[ \t]+Box|Postfach|Apartado(?:[ \t]+de[ \t]+correos)?|Caixa[ \t]+Postal|Boîte[ \t]+postale|а\/я|абонентский[ \t]+ящик)[ \t]*(?:#[ \t]*)?\d{1,10}(?!\p{N})/giu,
|
|
110
|
+
emit,
|
|
111
|
+
personal('ADDRESS', 'po-box')
|
|
112
|
+
);
|
|
113
|
+
// `skype: dean.malmgren`, `contact me on skype (dean.malmgren)`.
|
|
114
|
+
collectMatches(
|
|
115
|
+
text,
|
|
116
|
+
/(?<![\p{L}\p{N}_])(?:skype|discord|telegram|signal|wechat|whatsapp|keybase|matrix|mastodon|twitter|github|gitlab|instagram|tiktok)(?:[ \t]+(?:id|name|handle|username|user))?[ \t]*(?:[:=][ \t]*|\([ \t]*)(@?[\p{L}\p{N}_][\p{L}\p{N}_.:-]{1,63}[\p{L}\p{N}_])(?=[ \t]*\)|[\s,;!?]|\.(?:\s|$)|$)/giu,
|
|
117
|
+
emit,
|
|
118
|
+
personal('USERNAME', 'messenger-label', 0.85),
|
|
119
|
+
1
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// "login snoop pw biggreenhat", "u: snoop\np: biggreenhat",
|
|
124
|
+
// "user: thislibrary\npass: …": a username label followed by a password
|
|
125
|
+
// label marks both values; a lone `user:` is a chat role, not an account.
|
|
126
|
+
// Labels with a colon may be short (`u:`/`p:`); bare labels must be the
|
|
127
|
+
// unambiguous `login`/`username` and `pw`/`pwd`/`password` words.
|
|
128
|
+
const pairSeparator = '[ \\t]*[,;]?[ \\t]*(?:\\r?\\n[ \\t]*)?';
|
|
129
|
+
const credentialPair = new RegExp(
|
|
130
|
+
[
|
|
131
|
+
`(?:login|user(?:name)?|u)[ \\t]*[:=][ \\t]*([^\\s:=,;]{2,64})${pairSeparator}(?:pass(?:word)?|passwd|pwd|pw|p)[ \\t]*[:=][ \\t]*(\\S{4,128})`,
|
|
132
|
+
`(?:login|username)[ \\t]+([^\\s:=,;]{2,64})${pairSeparator}(?:password|passwd|pwd|pw)[ \\t]+(\\S{4,128})`,
|
|
133
|
+
]
|
|
134
|
+
.map((form) => `(?<![\\p{L}\\p{N}_])${form}`)
|
|
135
|
+
.join('|'),
|
|
136
|
+
'giu'
|
|
137
|
+
);
|
|
138
|
+
const placeholder = /^\[[\p{L}\p{N}_ -]+\]$/u;
|
|
139
|
+
function detectCredentialPairs(text, emit) {
|
|
140
|
+
for (const match of text.matchAll(credentialPair)) {
|
|
141
|
+
const user = match[1] ?? match[3];
|
|
142
|
+
const secret = match[2] ?? match[4];
|
|
143
|
+
if (
|
|
144
|
+
placeholder.test(user) ||
|
|
145
|
+
placeholder.test(secret) ||
|
|
146
|
+
credentialPlaceholder(secret)
|
|
147
|
+
) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const userStart = match.index + match[0].indexOf(user);
|
|
151
|
+
const secretStart = match.index + match[0].lastIndexOf(secret);
|
|
152
|
+
emit({
|
|
153
|
+
start: userStart,
|
|
154
|
+
end: userStart + user.length,
|
|
155
|
+
...personal('USERNAME', 'credential-pair', 0.9),
|
|
156
|
+
});
|
|
157
|
+
emit({
|
|
158
|
+
start: secretStart,
|
|
159
|
+
end: secretStart + secret.length,
|
|
160
|
+
type: 'SECRET',
|
|
161
|
+
category: 'credential',
|
|
162
|
+
rule: 'credential-pair',
|
|
163
|
+
confidence: 0.9,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// "my AMEX card: 1234 567890 12345": a labelled card keeps its label even when
|
|
169
|
+
// the number is a documentation placeholder that fails Luhn.
|
|
170
|
+
function detectLabelledCards(text, emit) {
|
|
171
|
+
collectMatches(
|
|
172
|
+
text,
|
|
173
|
+
/(?<![\p{L}\p{N}_])(?:card|visa|mastercard|amex|discover|diners|jcb|unionpay|maestro|карта)(?:[ \t]+(?:no\.?|number|num|#))?[ \t]*(?:[:#][ \t]*)?((?:\d[ -]?){12,18}\d)(?![\p{N}]|[ -]\d)/giu,
|
|
174
|
+
emit,
|
|
175
|
+
personal('CREDIT_CARD', 'card-context', 0.9),
|
|
176
|
+
1
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// "SSN 030 72 7381", "social security number: 003 06-8815": the label
|
|
181
|
+
// carries spaced and mixed separators that the bare dashed form omits.
|
|
182
|
+
function detectLabelledSsn(text, emit) {
|
|
183
|
+
collectMatches(
|
|
184
|
+
text,
|
|
185
|
+
/(?<![\p{L}\p{N}_])(?:SSN|SS#|social[ \t]+security(?:[ \t]+(?:number|no\.?|#))?)[ \t]*(?:(?:[:#]|is)[ \t]*)?(\d{3}[ .-]?\d{2}[ .-]?\d{4})(?![\p{N}]|[ .-]\d)/giu,
|
|
186
|
+
emit,
|
|
187
|
+
personal('SSN', 'ssn-label', 0.9),
|
|
188
|
+
1,
|
|
189
|
+
(value) => !/^(?:000|666|9\d\d)|^\d{3}\D?00|0000$/.test(value)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const states =
|
|
194
|
+
'AL|AK|AZ|AR|CA|CO|CT|DE|DC|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|PR';
|
|
195
|
+
function detectPostalCodes(text, emit) {
|
|
196
|
+
// "ZIP 94105", "ZIPs 10001 and 30301", "zip code: 94105-1234".
|
|
197
|
+
for (const label of text.matchAll(
|
|
198
|
+
/(?<![\p{L}\p{N}_])(?:ZIP(?:[ \t]?codes?)?s?|postal[ \t]+codes?|PLZ|CEP)[ \t]*(?:[:#][ \t]*)?/giu
|
|
199
|
+
)) {
|
|
200
|
+
const start = label.index + label[0].length;
|
|
201
|
+
const list =
|
|
202
|
+
/^\d{5}(?:-\d{4})?(?:(?:[ \t]*,[ \t]*|[ \t]+(?:and|or|&)[ \t]+)\d{5}(?:-\d{4})?)*(?![\p{L}\p{N}_-])/u.exec(
|
|
203
|
+
text.slice(start, start + 200)
|
|
204
|
+
);
|
|
205
|
+
for (const code of list?.[0].matchAll(/\d{5}(?:-\d{4})?/g) ?? []) {
|
|
206
|
+
emit({
|
|
207
|
+
start: start + code.index,
|
|
208
|
+
end: start + code.index + code[0].length,
|
|
209
|
+
...personal('POSTAL_CODE', 'postal-label', 0.9),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// "New York City, NY 10002": a state abbreviation anchors the ZIP code.
|
|
214
|
+
collectMatches(
|
|
215
|
+
text,
|
|
216
|
+
new RegExp(
|
|
217
|
+
`(?<=[\\p{L}.],)[ \\t]*(?:${states})[ \\t]+(\\d{5}(?:-\\d{4})?)(?![\\p{L}\\p{N}_-])`,
|
|
218
|
+
'gu'
|
|
219
|
+
),
|
|
220
|
+
emit,
|
|
221
|
+
personal('POSTAL_CODE', 'state-zip', 0.85),
|
|
222
|
+
1
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function detectPersonalEvidence(text, emit) {
|
|
227
|
+
detectNames(text, emit);
|
|
228
|
+
detectContactForms(text, emit);
|
|
229
|
+
detectCredentialPairs(text, emit);
|
|
230
|
+
detectLabelledCards(text, emit);
|
|
231
|
+
detectLabelledSsn(text, emit);
|
|
232
|
+
detectPostalCodes(text, emit);
|
|
233
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Registers the personal-data detectors and fake generators. The main entry
|
|
2
|
+
// imports this module for its side effect; the `./credentials` entry does not,
|
|
3
|
+
// which keeps faker, libphonenumber and these detectors out of its start-up.
|
|
4
|
+
import { registerFeatures } from './features.js';
|
|
5
|
+
import { detectPersonal } from './personal.js';
|
|
6
|
+
import { detectCatalog } from './entities.js';
|
|
7
|
+
import { detectIdentity, fakeIdentityValue } from './identity.js';
|
|
8
|
+
import { detectNational } from './national.js';
|
|
9
|
+
import { detectPersonalContext } from './personal-context.js';
|
|
10
|
+
import { detectPersonalEvidence } from './personal-evidence.js';
|
|
11
|
+
import { detectDocumentEntities } from './document-entities.js';
|
|
12
|
+
import {
|
|
13
|
+
detectProseAndDocuments,
|
|
14
|
+
validateProperNames,
|
|
15
|
+
} from './proper-names.js';
|
|
16
|
+
import { detectStrict } from './strict.js';
|
|
17
|
+
import { realisticFake } from './fake.js';
|
|
18
|
+
import { prepareFakeText } from './fake-entities.js';
|
|
19
|
+
import { fakeDocumentSpans } from './specimens.js';
|
|
20
|
+
|
|
21
|
+
function detectPersonalData(text, emit, options) {
|
|
22
|
+
detectPersonal(text, emit);
|
|
23
|
+
detectProseAndDocuments(text, emit, options, detectDocumentEntities);
|
|
24
|
+
detectCatalog(text, emit);
|
|
25
|
+
detectIdentity(text, emit);
|
|
26
|
+
detectNational(text, emit);
|
|
27
|
+
detectPersonalContext(text, emit);
|
|
28
|
+
detectPersonalEvidence(text, emit);
|
|
29
|
+
detectStrict(text, emit, options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
registerFeatures({
|
|
33
|
+
detectPersonalData,
|
|
34
|
+
validateProperNames,
|
|
35
|
+
realisticFake,
|
|
36
|
+
prepareFakeText,
|
|
37
|
+
fakeDocumentSpans,
|
|
38
|
+
fakeIdentityValue,
|
|
39
|
+
});
|
package/src/personal.js
CHANGED
|
@@ -3,6 +3,7 @@ import { firstNames } from './names.js';
|
|
|
3
3
|
import { labels } from './rules.js';
|
|
4
4
|
import { transliteratedGivenNames } from './transliteration.js';
|
|
5
5
|
import { serviceAccounts } from './public.js';
|
|
6
|
+
import { isCommonProseWord } from './prose-words.js';
|
|
6
7
|
const labelWords = new Set(Object.values(labels).flat());
|
|
7
8
|
|
|
8
9
|
const details = (type, rule, confidence = 0.85) => ({
|
|
@@ -11,6 +12,12 @@ const details = (type, rule, confidence = 0.85) => ({
|
|
|
11
12
|
category: 'personal',
|
|
12
13
|
confidence,
|
|
13
14
|
});
|
|
15
|
+
// "John C Smith", "John A. Smith": a Latin-script middle initial. A bare "I"
|
|
16
|
+
// is the pronoun ("Will I Go"), so it needs its period.
|
|
17
|
+
const initial = (language) =>
|
|
18
|
+
['ar', 'he', 'hi', 'th', 'ja', 'zh', 'ko'].includes(language)
|
|
19
|
+
? ''
|
|
20
|
+
: '(?:[ \\t]+(?:\\p{Lu}\\.|(?!I[ \\t])\\p{Lu}(?=[ \\t])))?';
|
|
14
21
|
const names = Object.entries(firstNames).map(([language, list]) => {
|
|
15
22
|
const words = list.split(' ').map(escapePattern).join('|');
|
|
16
23
|
const surname = ['ar', 'he', 'hi', 'th'].includes(language)
|
|
@@ -18,7 +25,7 @@ const names = Object.entries(firstNames).map(([language, list]) => {
|
|
|
18
25
|
: "[\\p{Lu}][\\p{L}\\p{M}’'-]{1,32}";
|
|
19
26
|
const pattern = ['ja', 'zh', 'ko'].includes(language)
|
|
20
27
|
? `(?:${words})[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]{1,4}`
|
|
21
|
-
: `(?:${words})[ \\t]+${surname}${language === 'vi' ? `(?:[ \\t]+${surname})?` : ''}`;
|
|
28
|
+
: `(?:${words})${initial(language)}[ \\t]+${surname}${language === 'vi' ? `(?:[ \\t]+${surname})?` : ''}`;
|
|
22
29
|
return new RegExp(`(?<![\\p{L}\\p{M}])${pattern}(?![\\p{L}\\p{M}])`, 'gu');
|
|
23
30
|
});
|
|
24
31
|
|
|
@@ -33,7 +40,7 @@ const ibanLengths = Object.fromEntries(
|
|
|
33
40
|
const mentionedEmail =
|
|
34
41
|
/^\/[\p{L}\p{N}_.!#$%&'*+?^`{|}~-]+@[\p{L}\p{N}][\p{L}\p{N}.-]{0,252}\.\p{L}{2,63}(?![\p{L}\p{N}_-])/u;
|
|
35
42
|
export function iban(value) {
|
|
36
|
-
const compact = value.replace(
|
|
43
|
+
const compact = value.replace(/[\s-]/g, '').toUpperCase();
|
|
37
44
|
if (
|
|
38
45
|
!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(compact) ||
|
|
39
46
|
compact.length !== ibanLengths[compact.slice(0, 2)]
|
|
@@ -53,7 +60,7 @@ export function iban(value) {
|
|
|
53
60
|
|
|
54
61
|
function detectIban(text, emit) {
|
|
55
62
|
for (const match of text.matchAll(
|
|
56
|
-
/(?<![A-Z0-9])[A-Z]{2}
|
|
63
|
+
/(?<![A-Z0-9])[A-Z]{2} ?\d{2}(?:[ -]?[A-Z0-9]){8,40}/g
|
|
57
64
|
)) {
|
|
58
65
|
const needed = ibanLengths[match[0].slice(0, 2)];
|
|
59
66
|
if (!needed) {
|
|
@@ -62,7 +69,7 @@ function detectIban(text, emit) {
|
|
|
62
69
|
let end = 0,
|
|
63
70
|
count = 0;
|
|
64
71
|
while (end < match[0].length && count < needed) {
|
|
65
|
-
if (match[0][end]
|
|
72
|
+
if (!/[ -]/.test(match[0][end])) {
|
|
66
73
|
count++;
|
|
67
74
|
}
|
|
68
75
|
end++;
|
|
@@ -180,11 +187,46 @@ function detectDocumentNames(text, emit) {
|
|
|
180
187
|
}
|
|
181
188
|
const russianFull =
|
|
182
189
|
/(?<![\p{L}])(?:[А-ЯЁ][а-яё]{2,30}[ \t]+){2}[А-ЯЁ][а-яё]{2,30}(?:вич|вна|ична)|(?<![\p{L}])(?:[А-ЯЁ][а-яё]{2,30}[ \t]+){2}(?:оглы|кызы)(?![\p{L}])/gu;
|
|
190
|
+
// "My ITIN", "Our BIN": a possessive word or a short all-capital acronym
|
|
191
|
+
// matches the Slavic surname shape but carries no name evidence.
|
|
192
|
+
const possessiveWord =
|
|
193
|
+
/^(?:my|our|your|his|her|their|its|the|this|that|мой|моя|наш|наша|ваш|ваша)$/iu;
|
|
194
|
+
function slavicPairEvidence(value) {
|
|
195
|
+
const words = value.split(/[ \t]+/);
|
|
196
|
+
return !words.some(
|
|
197
|
+
(word) =>
|
|
198
|
+
possessiveWord.test(word) ||
|
|
199
|
+
/^\p{Lu}{2,4}$/u.test(word) ||
|
|
200
|
+
isCommonProseWord(word)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
// A rejected pair is retried from its second word, so `Кандидат Петров Иван`
|
|
204
|
+
// still finds `Петров Иван` after the cue word.
|
|
205
|
+
function detectSlavicPairs(text, emit) {
|
|
206
|
+
const pattern = new RegExp(slavicPairs.source, 'gu');
|
|
207
|
+
let match;
|
|
208
|
+
while ((match = pattern.exec(text))) {
|
|
209
|
+
const value = match[0];
|
|
210
|
+
if (
|
|
211
|
+
value !== 'REDACTED' &&
|
|
212
|
+
!placeWord.test(value.split(/[ \t]+/).at(-1)) &&
|
|
213
|
+
slavicPairEvidence(value)
|
|
214
|
+
) {
|
|
215
|
+
emit({
|
|
216
|
+
start: match.index,
|
|
217
|
+
end: match.index + value.length,
|
|
218
|
+
...details('PERSON', 'name-identity', 0.9),
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
pattern.lastIndex = match.index + value.search(/[ \t]/u) + 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
183
225
|
export function detectPersonal(text, emit) {
|
|
184
226
|
detectDocumentNames(text, emit);
|
|
227
|
+
detectSlavicPairs(text, emit);
|
|
185
228
|
for (const pattern of [
|
|
186
229
|
latinPairs,
|
|
187
|
-
slavicPairs,
|
|
188
230
|
russianFull,
|
|
189
231
|
/(?<![\p{L}])(?:[А-ЯЁ][а-яё]{2,30}[ \t]+)[А-ЯЁ][а-яё]{2,30}(?:вич|вна|ична)[ \t]+[А-ЯЁ][а-яё]{2,30}(?![\p{L}])/gu,
|
|
190
232
|
]) {
|
|
@@ -218,21 +260,35 @@ export function detectPersonal(text, emit) {
|
|
|
218
260
|
);
|
|
219
261
|
collectMatches(
|
|
220
262
|
text,
|
|
221
|
-
/(?<![\d(])(?:[78][ -]?)?\(\d{3}\)[ -]?\d{3}[ -]\d{2}[ -]?\d{2}(?!\d)|(
|
|
263
|
+
/(?<![\d(])(?:[78][ -]?)?\(\d{3}\)[ -]?\d{3}[ -]\d{2}[ -]?\d{2}(?!\d)|(?<![\d-])(?:(?<![\p{L}_.])1[.-])?(?:\d{3}[.-]){2}\d{4}(?!\d|-[\p{L}\p{N}])/gu,
|
|
222
264
|
emit,
|
|
223
265
|
details('PHONE', 'national-phone')
|
|
224
266
|
);
|
|
225
267
|
collectMatches(
|
|
226
268
|
text,
|
|
227
|
-
/(?<![\p{L}\p{N}])\d{1,6}[A-Za-z]?[ \t]+(?:[\p{Lu}][\p{L}.'-]*[ \t]+){1,5}(?:Street|St|Road|Rd|Avenue|Ave|Lane|Ln|Drive|Dr|Boulevard|Blvd|Way|Straße|Strasse|Rue|Calle|Rua)\b(?:,[ \t]+(?:(?:[^ \t\r\n<>][^\r\n<>]{0,79}?[ \t]+)?(?:[A-Z]{1,2}\d[A-Z\d]?[ \t]+\d[A-Z]{2}|\d{5}(?:-\d{4})?)\b|(?:[\p{Lu}][\p{Ll}.'-]+[ \t]*){1,4}))?/gu,
|
|
269
|
+
/(?<![\p{L}\p{N}])\d{1,6}[A-Za-z]?[ \t]+(?:[\p{Lu}][\p{L}.'-]*[ \t]+){1,5}(?:Street|St|Road|Rd|Avenue|Ave|Lane|Ln|Drive|Dr|Boulevard|Blvd|Way|Terrace|Court|Ct|Place|Pl|Parkway|Pkwy|Circle|Highway|Hwy|Square|Crescent|Close|Straße|Strasse|Rue|Calle|Rua)\b(?:,?[ \t]+(?:Apt|Apartment|Suite|Ste|Unit|Floor|Fl|Room|Rm)\.?[ \t]*#?[A-Za-z0-9-]{1,6}\b|[ \t]+#[A-Za-z0-9-]{1,6}\b)?(?:,[ \t]+(?:(?:[^ \t\r\n<>][^\r\n<>]{0,79}?[ \t]+)?(?:[A-Z]{1,2}\d[A-Z\d]?[ \t]+\d[A-Z]{2}|\d{5}(?:-\d{4})?)\b|(?:[\p{Lu}][\p{Ll}.'-]+[ \t]*){1,4}))?/gu,
|
|
228
270
|
emit,
|
|
229
271
|
details('ADDRESS', 'postal-address')
|
|
230
272
|
);
|
|
273
|
+
// "Mr. O'Brien", "Dr Jane Smith": a courtesy title is name evidence;
|
|
274
|
+
// after a number, "30 Ms Retry" is a millisecond unit.
|
|
275
|
+
collectMatches(
|
|
276
|
+
text,
|
|
277
|
+
/(?<![\p{L}\p{N}_])(?:Mr|Mrs|Ms|Mx|Miss|Dr|Prof)(?<!\p{N}[ \t]*\p{L}+)\.?[ \t]+(\p{Lu}(?:['’]\p{Lu})?[\p{Ll}\p{M}]+(?:-\p{Lu}[\p{Ll}\p{M}]+)?(?:[ \t]+\p{Lu}(?:['’]\p{Lu})?[\p{Ll}\p{M}]+(?:-\p{Lu}[\p{Ll}\p{M}]+)?)?)(?![\p{L}\p{M}])/gu,
|
|
278
|
+
emit,
|
|
279
|
+
details('PERSON', 'honorific-name'),
|
|
280
|
+
1,
|
|
281
|
+
(value) => !placeWord.test(value.split(/[ \t]+/).at(-1))
|
|
282
|
+
);
|
|
231
283
|
collectMatches(
|
|
232
284
|
text,
|
|
233
|
-
|
|
285
|
+
// Colon, dash or Cisco dotted notation with one separator; the broadcast
|
|
286
|
+
// and all-zero addresses identify no device.
|
|
287
|
+
/(?<![\da-fA-F:.-])(?:[\da-fA-F]{2}([:-])(?:[\da-fA-F]{2}\1){4}[\da-fA-F]{2}|[\da-fA-F]{4}\.[\da-fA-F]{4}\.[\da-fA-F]{4})(?![\da-fA-F:-]|\.[\da-fA-F])/g,
|
|
234
288
|
emit,
|
|
235
|
-
details('MAC_ADDRESS', 'mac')
|
|
289
|
+
details('MAC_ADDRESS', 'mac'),
|
|
290
|
+
0,
|
|
291
|
+
(value) => !/^(?:[fF]{2}[:.-]?|0{2}[:.-]?)+$/.test(value.replace(/\./g, ''))
|
|
236
292
|
);
|
|
237
293
|
collectMatches(
|
|
238
294
|
text,
|
|
@@ -249,7 +305,7 @@ export function detectPersonal(text, emit) {
|
|
|
249
305
|
);
|
|
250
306
|
collectMatches(
|
|
251
307
|
text,
|
|
252
|
-
/(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)([^/\\\s"'<>[\]]+)/g,
|
|
308
|
+
/(?:\/Users\/|\/home\/|[A-Za-z]:\\(?:Users|Documents and Settings|(?:Windows|WINNT)\\Profiles)\\)([^/\\\s"'<>[\]]+)/g,
|
|
253
309
|
emit,
|
|
254
310
|
details('USERNAME', 'home-path', 0.95),
|
|
255
311
|
1,
|