@link-foundation/sensitive-data-sanitizer 0.17.1 → 0.19.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 +15 -0
- package/README.md +25 -0
- package/bin/sensitive-data-sanitizer.js +8 -0
- package/docs/API.md +266 -34
- package/docs/COVERAGE.md +135 -13
- package/docs/RULES.md +403 -0
- package/examples/custom-log-policy.mjs +23 -0
- package/examples/prose-logs.mjs +16 -0
- package/package.json +18 -2
- package/src/compiled-known.js +55 -0
- package/src/context.js +9 -1
- package/src/detection.js +11 -1
- package/src/document-catalog.js +246 -0
- package/src/document-dates.js +15 -0
- package/src/document-entities.js +27 -14
- package/src/document-experience.js +115 -0
- package/src/document-extras.js +96 -64
- package/src/document-fields.js +3 -1
- package/src/document-vocabulary.js +80 -68
- package/src/engines.js +93 -36
- package/src/index.d.ts +57 -4
- package/src/index.js +5 -0
- package/src/jsonl-stream.js +2 -5
- package/src/ner.d.ts +5 -0
- package/src/ner.js +96 -0
- package/src/personal-variants.js +29 -8
- package/src/placeholder-types.js +1 -0
- package/src/policy.js +107 -0
- package/src/propagation.js +5 -1
- package/src/proper-names.js +222 -0
- package/src/record-worker.js +6 -0
- package/src/rule-catalog.js +669 -0
- package/src/sanitizer.js +114 -21
- package/src/stream-worker.js +5 -0
- package/src/stream.js +11 -6
- package/src/strict.js +66 -0
- package/src/structured.js +131 -59
- package/src/sync-detectors.js +38 -0
- package/src/sync-sanitizer.js +16 -0
- package/src/transforms.js +85 -7
- package/src/vocabulary.js +217 -0
- package/src/worker-options.js +12 -0
package/src/document-extras.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { collectMatches } from './detection.js';
|
|
2
|
+
import {
|
|
3
|
+
detectDatedEmployers,
|
|
4
|
+
isDocumentTechnology,
|
|
5
|
+
} from './document-experience.js';
|
|
6
|
+
import { isDocumentShareLink } from './proper-names.js';
|
|
2
7
|
|
|
3
8
|
const boundary = '[\\p{L}\\p{N}_]';
|
|
4
9
|
const proper = '[\\p{Lu}][\\p{L}\\p{M}\\p{N}.-]{0,46}[\\p{L}\\p{M}\\p{N}]';
|
|
5
10
|
const name = `${proper}(?:[ \\t]+${proper}){0,5}`;
|
|
11
|
+
const employerName = `(?:«[^«»\\r\\n]{1,160}»|“[^“”\\r\\n]{1,160}”|"[^"\\r\\n]{1,160}"|${name})`;
|
|
6
12
|
const orgList = `(?:«[^«»\\r\\n]{1,160}»|${name})(?:[ \\t]*(?:,|и|and)[ \\t]+${name}){0,5}`;
|
|
7
13
|
const cues = new RegExp(
|
|
8
14
|
`(?<!${boundary})(?:компании|опыт[ \\t]+в|проекты[ \\t]+для|[Рр]оль[ \\t]+в)[ \\t]+(${orgList})(?!${boundary})`,
|
|
@@ -12,13 +18,42 @@ const duration = new RegExp(
|
|
|
12
18
|
`(?<!${boundary})(${name})(?=[ \\t]*\\(\\d{1,2}[ \\t]+(?:год|года|лет|years?)\\))`,
|
|
13
19
|
'gu'
|
|
14
20
|
);
|
|
15
|
-
|
|
16
|
-
const role =
|
|
17
|
-
/^(?:менеджер|директор|руководитель|инженер|аналитик|разработчик)(?![\p{L}])|^(?:manager|director|engineer|developer|senior|qa)\b/iu;
|
|
18
|
-
|
|
19
|
-
export function detectDocumentExtras(text, emit, range) {
|
|
21
|
+
export function detectDocumentExtras(text, emit, range, debug, options) {
|
|
20
22
|
for (const [type, rule, pattern, group] of [
|
|
21
23
|
['PHONE', 'document-mobile-bare', /^[78]9\d{9}$/gm, 0],
|
|
24
|
+
[
|
|
25
|
+
'PHONE',
|
|
26
|
+
'document-mobile-comma',
|
|
27
|
+
/,[ \t]*([78]9\d{9})(?![\p{L}\p{N}_])/gu,
|
|
28
|
+
1,
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
'ORGANIZATION',
|
|
32
|
+
'organization-group-cue',
|
|
33
|
+
new RegExp(
|
|
34
|
+
`(?<!${boundary})(?:ГК|[Гг]руппа компаний)[ \\t]+${employerName}`,
|
|
35
|
+
'gu'
|
|
36
|
+
),
|
|
37
|
+
0,
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'EDUCATION',
|
|
41
|
+
'education-bracket',
|
|
42
|
+
/(?<=\()(МГУ|СПбГУ|МГТУ|РГГУ|ВШЭ)(?=[,)])/gu,
|
|
43
|
+
1,
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
'EDUCATION',
|
|
47
|
+
'education-named-acronym',
|
|
48
|
+
/(?<![\p{L}\p{N}_])(?:МГУ|СПбГУ|МГТУ|РГГУ|ВШЭ)[ \t]+им\.[ \t]+[А-ЯЁ][а-яё-]{2,40}/gu,
|
|
49
|
+
0,
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
'EDUCATION',
|
|
53
|
+
'education-training',
|
|
54
|
+
/(?<![\p{L}\p{N}_])(?:Обучение|Training)[ \t]*:[ \t]*([\p{Lu}][\p{L}\p{M}.-]{1,47})/gu,
|
|
55
|
+
1,
|
|
56
|
+
],
|
|
22
57
|
[
|
|
23
58
|
'PHONE',
|
|
24
59
|
'document-mobile-context',
|
|
@@ -56,7 +91,8 @@ export function detectDocumentExtras(text, emit, range) {
|
|
|
56
91
|
pattern,
|
|
57
92
|
emit,
|
|
58
93
|
{ type, rule, category: 'personal', confidence: 0.85 },
|
|
59
|
-
group
|
|
94
|
+
group,
|
|
95
|
+
(value) => type !== 'ORGANIZATION' || !isDocumentTechnology(value)
|
|
60
96
|
);
|
|
61
97
|
}
|
|
62
98
|
for (const match of text.matchAll(cues)) {
|
|
@@ -64,6 +100,9 @@ export function detectDocumentExtras(text, emit, range) {
|
|
|
64
100
|
for (const company of match[1].matchAll(
|
|
65
101
|
new RegExp(`«[^«»\\r\\n]{1,160}»|${name}`, 'gu')
|
|
66
102
|
)) {
|
|
103
|
+
if (isDocumentTechnology(company[0])) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
67
106
|
emit({
|
|
68
107
|
start: offset + company.index,
|
|
69
108
|
end: offset + company.index + company[0].length,
|
|
@@ -75,7 +114,57 @@ export function detectDocumentExtras(text, emit, range) {
|
|
|
75
114
|
}
|
|
76
115
|
}
|
|
77
116
|
detectQueryContacts(text, emit);
|
|
78
|
-
|
|
117
|
+
collectMatches(
|
|
118
|
+
text,
|
|
119
|
+
/https?:\/\/[^\s"<>\\]+/gu,
|
|
120
|
+
emit,
|
|
121
|
+
{
|
|
122
|
+
type: 'URL',
|
|
123
|
+
category: 'personal',
|
|
124
|
+
rule: 'document-share-path',
|
|
125
|
+
confidence: 0.85,
|
|
126
|
+
},
|
|
127
|
+
0,
|
|
128
|
+
isDocumentShareLink
|
|
129
|
+
);
|
|
130
|
+
detectEmployerContinuation(text, emit);
|
|
131
|
+
detectDatedEmployers(text, emit, range, debug, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function detectEmployerContinuation(text, emit) {
|
|
135
|
+
const continuation = new RegExp(
|
|
136
|
+
`,[ \\t]*(?:потом|затем)[ \\t]+в[ \\t]+(${employerName})`,
|
|
137
|
+
'uy'
|
|
138
|
+
);
|
|
139
|
+
for (const match of text.matchAll(
|
|
140
|
+
new RegExp(
|
|
141
|
+
`(?<!${boundary})[Рр]аботал[аи]?[ \\t]+в[ \\t]+(${employerName})`,
|
|
142
|
+
'gu'
|
|
143
|
+
)
|
|
144
|
+
)) {
|
|
145
|
+
continuation.lastIndex = match.index + match[0].length;
|
|
146
|
+
let next = continuation.exec(text);
|
|
147
|
+
if (next) {
|
|
148
|
+
emitEmployer(match);
|
|
149
|
+
}
|
|
150
|
+
while (next) {
|
|
151
|
+
emitEmployer(next);
|
|
152
|
+
next = continuation.exec(text);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function emitEmployer(match) {
|
|
156
|
+
const end = match.index + match[0].length;
|
|
157
|
+
if (!isDocumentTechnology(match[1])) {
|
|
158
|
+
emit({
|
|
159
|
+
start: end - match[1].length,
|
|
160
|
+
end,
|
|
161
|
+
type: 'ORGANIZATION',
|
|
162
|
+
category: 'personal',
|
|
163
|
+
rule: 'organization-continuation',
|
|
164
|
+
confidence: 0.85,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
79
168
|
}
|
|
80
169
|
|
|
81
170
|
function detectQueryContacts(text, emit) {
|
|
@@ -111,60 +200,3 @@ function detectQueryContacts(text, emit) {
|
|
|
111
200
|
});
|
|
112
201
|
}
|
|
113
202
|
}
|
|
114
|
-
|
|
115
|
-
function detectExperience(text, emit, range) {
|
|
116
|
-
// Date anchors bound each entry; do not guess at undated employer/title fragments.
|
|
117
|
-
collectMatches(
|
|
118
|
-
text,
|
|
119
|
-
new RegExp(
|
|
120
|
-
`(?:^|[\\r\\n])[ \\t]*[^|\\r\\n]{1,120}\\|[ \\t]*(${name})[ \\t]*\\|[ \\t]*${range}`,
|
|
121
|
-
'gu'
|
|
122
|
-
),
|
|
123
|
-
emit,
|
|
124
|
-
{
|
|
125
|
-
type: 'ORGANIZATION',
|
|
126
|
-
rule: 'organization-experience',
|
|
127
|
-
category: 'personal',
|
|
128
|
-
confidence: 0.85,
|
|
129
|
-
},
|
|
130
|
-
1
|
|
131
|
-
);
|
|
132
|
-
const dated = new RegExp(`([^\\r\\n;|]{1,320}?)\\((${range})\\)`, 'gu');
|
|
133
|
-
for (const match of text.matchAll(dated)) {
|
|
134
|
-
const prefix = match[1];
|
|
135
|
-
const content = prefix
|
|
136
|
-
.replace(/^.*\(\d[^)]*\)[, \t]*/, '')
|
|
137
|
-
.replace(/^[, \t]+/, '');
|
|
138
|
-
const chunks = [...content.matchAll(/[^,]+/g)];
|
|
139
|
-
const parts = chunks.map((part) => part[0].trim());
|
|
140
|
-
let value;
|
|
141
|
-
if (parts.length > 1 && role.test(parts[0])) {
|
|
142
|
-
let index = 1;
|
|
143
|
-
while (index < parts.length - 1 && /^[\p{Ll}]/u.test(parts[index])) {
|
|
144
|
-
index++;
|
|
145
|
-
}
|
|
146
|
-
value = content.slice(chunks[index].index).trim();
|
|
147
|
-
} else if (parts.length === 2 && role.test(parts[1])) {
|
|
148
|
-
value = parts[0].trim();
|
|
149
|
-
}
|
|
150
|
-
if (
|
|
151
|
-
!value ||
|
|
152
|
-
!/^[\p{Lu}«“"]/u.test(value) ||
|
|
153
|
-
value.includes('[REDACTED]')
|
|
154
|
-
) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const start = match.index + prefix.lastIndexOf(value);
|
|
158
|
-
if (start < match.index) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
emit({
|
|
162
|
-
start,
|
|
163
|
-
end: start + value.length,
|
|
164
|
-
type: 'ORGANIZATION',
|
|
165
|
-
category: 'personal',
|
|
166
|
-
rule: 'organization-experience',
|
|
167
|
-
confidence: 0.85,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
package/src/document-fields.js
CHANGED
|
@@ -15,5 +15,7 @@ const types = new Map(
|
|
|
15
15
|
)
|
|
16
16
|
);
|
|
17
17
|
export function documentFieldType(field) {
|
|
18
|
-
return types.get(field
|
|
18
|
+
return types.get(normalizeFieldName(field));
|
|
19
19
|
}
|
|
20
|
+
export const normalizeFieldName = (field) =>
|
|
21
|
+
field.replace(/[_-]/g, '').toLowerCase();
|
|
@@ -1,71 +1,83 @@
|
|
|
1
1
|
// Authored, deliberately finite vocabulary; no network or model downloads.
|
|
2
2
|
// Each row follows localeVocabulary: locale, legal forms, school types, places.
|
|
3
|
-
export const documentVocabulary =
|
|
3
|
+
export const documentVocabulary = Object.freeze(
|
|
4
4
|
[
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'
|
|
70
|
-
|
|
71
|
-
|
|
5
|
+
[
|
|
6
|
+
'en',
|
|
7
|
+
'Inc|LLC|Ltd|PLC|Corporation',
|
|
8
|
+
'University|College|Institute|School',
|
|
9
|
+
'Springfield|Riverton|London|New York|California',
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
'es',
|
|
13
|
+
'S.A.|S.L.|S.L.U.',
|
|
14
|
+
'Universidad|Colegio|Instituto|Escuela',
|
|
15
|
+
'Madrid|Barcelona|Andalucía',
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
'fr',
|
|
19
|
+
'SARL|SAS|SA|S.A.',
|
|
20
|
+
'Université|Collège|Institut|École',
|
|
21
|
+
'Paris|Lyon|Bretagne',
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
'de',
|
|
25
|
+
'GmbH|AG|KG|UG',
|
|
26
|
+
'Universität|Hochschule|Institut|Schule',
|
|
27
|
+
'Berlin|München|Bayern',
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
'pt',
|
|
31
|
+
'Ltda|S.A.|Unipessoal',
|
|
32
|
+
'Universidade|Faculdade|Instituto|Escola',
|
|
33
|
+
'Lisboa|São Paulo|Algarve',
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'ru',
|
|
37
|
+
'ООО|АО|ПАО|ИП|ЗАО',
|
|
38
|
+
'Университет|университет|Институт|институт|Школа|школа',
|
|
39
|
+
'Москва|Санкт-Петербург|Казань|Татарстан|Ярославль|Кострома|Новосибирск|Екатеринбург|Нижний Новгород|Самара|Омск|Челябинск|Ростов-на-Дону|Уфа|Красноярск|Пермь|Воронеж|Волгоград|Саратов|Тюмень|Тольятти|Ижевск|Барнаул|Иркутск|Хабаровск|Владивосток|Махачкала|Томск|Оренбург|Кемерово|Рязань|Астрахань|Пенза|Липецк|Киров|Чебоксары|Тула|Калининград|Курск|Улан-Удэ|Ставрополь|Сочи|Тверь|Брянск|Белгород|Владимир|Архангельск|Калуга|Смоленск|Вологда|Саранск|Орёл',
|
|
40
|
+
],
|
|
41
|
+
['ja', '株式会社|有限会社|合同会社', '大学|学院|学校', '東京|大阪|北海道'],
|
|
42
|
+
['zh', '有限公司|股份有限公司', '大学|学院|学校', '北京|上海|广东'],
|
|
43
|
+
[
|
|
44
|
+
'zh-Hant',
|
|
45
|
+
'有限公司|股份有限公司',
|
|
46
|
+
'大學|學院|學校',
|
|
47
|
+
'臺北|台北|高雄|臺灣',
|
|
48
|
+
],
|
|
49
|
+
['ko', '주식회사|유한회사', '대학교|대학|학교', '서울|부산|경기도'],
|
|
50
|
+
['ar', 'ذ.م.م|شركة', 'جامعة|معهد|مدرسة', 'القاهرة|دبي|الرياض'],
|
|
51
|
+
[
|
|
52
|
+
'he',
|
|
53
|
+
'בע״מ|בע"מ|חברה',
|
|
54
|
+
'אוניברסיטת|אוניברסיטה|מכללת|מכון',
|
|
55
|
+
'ירושלים|תל אביב|חיפה',
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
'hi',
|
|
59
|
+
'प्राइवेट लिमिटेड|लिमिटेड',
|
|
60
|
+
'विश्वविद्यालय|महाविद्यालय|संस्थान',
|
|
61
|
+
'दिल्ली|मुंबई|महाराष्ट्र',
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
'tr',
|
|
65
|
+
'A.Ş.|Ltd. Şti.',
|
|
66
|
+
'Üniversitesi|Üniversite|Enstitüsü|Okulu',
|
|
67
|
+
'İstanbul|Ankara|İzmir',
|
|
68
|
+
],
|
|
69
|
+
['id', 'PT|CV|Tbk', 'Universitas|Institut|Sekolah', 'Jakarta|Bandung|Bali'],
|
|
70
|
+
[
|
|
71
|
+
'vi',
|
|
72
|
+
'TNHH|CTCP',
|
|
73
|
+
'Đại học|Trường|Học viện',
|
|
74
|
+
'Hà Nội|Hồ Chí Minh|Đà Nẵng',
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
'th',
|
|
78
|
+
'บริษัท|จำกัด',
|
|
79
|
+
'มหาวิทยาลัย|วิทยาลัย|โรงเรียน',
|
|
80
|
+
'กรุงเทพมหานคร|เชียงใหม่|ภูเก็ต',
|
|
81
|
+
],
|
|
82
|
+
].map((row) => Object.freeze(row))
|
|
83
|
+
);
|
package/src/engines.js
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { failure, internalFailure } from './detection.js';
|
|
17
17
|
import { projectText } from './projection.js';
|
|
18
18
|
import { decodedRuns } from './encoded.js';
|
|
19
|
+
import { hasCustomTransforms } from './transforms.js';
|
|
20
|
+
import { builtInEnabled } from './policy.js';
|
|
19
21
|
import {
|
|
20
22
|
initializePlaceholders,
|
|
21
23
|
placeholderKnown,
|
|
@@ -70,7 +72,8 @@ async function secretlintDetect(text) {
|
|
|
70
72
|
}));
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
const secretlintEngine = { detect: secretlintDetect };
|
|
76
|
+
async function detect(engine, text, limit, options) {
|
|
74
77
|
try {
|
|
75
78
|
const found = await engine.detect(text);
|
|
76
79
|
if (!Array.isArray(found)) {
|
|
@@ -79,7 +82,12 @@ async function detect(engine, text, limit) {
|
|
|
79
82
|
if (found.length > limit) {
|
|
80
83
|
throw failure('ERR_LIMIT');
|
|
81
84
|
}
|
|
82
|
-
return found
|
|
85
|
+
return found
|
|
86
|
+
.map((finding) => sanitizeFinding(finding, text.length))
|
|
87
|
+
.filter(
|
|
88
|
+
(finding) =>
|
|
89
|
+
engine !== secretlintEngine || builtInEnabled(finding, options)
|
|
90
|
+
);
|
|
83
91
|
} catch {
|
|
84
92
|
throw failure('ERR_ENGINE');
|
|
85
93
|
}
|
|
@@ -88,7 +96,7 @@ async function detect(engine, text, limit) {
|
|
|
88
96
|
async function engineFindings(engine, projection, options, emit) {
|
|
89
97
|
const limit = options.maxFindings ?? 100000;
|
|
90
98
|
if (projection.changed) {
|
|
91
|
-
for (const f of await detect(engine, projection.text, limit)) {
|
|
99
|
+
for (const f of await detect(engine, projection.text, limit, options)) {
|
|
92
100
|
emit({
|
|
93
101
|
...f,
|
|
94
102
|
start: projection.starts[f.start],
|
|
@@ -106,7 +114,7 @@ async function engineFindings(engine, projection, options, emit) {
|
|
|
106
114
|
const view = projectText(run.text).text;
|
|
107
115
|
const decoded = markFake(
|
|
108
116
|
view,
|
|
109
|
-
await detect(engine, view, limit),
|
|
117
|
+
await detect(engine, view, limit, options),
|
|
110
118
|
options
|
|
111
119
|
).filter((f) => actionable(f, options));
|
|
112
120
|
if (!decoded.length) {
|
|
@@ -124,6 +132,28 @@ async function engineFindings(engine, projection, options, emit) {
|
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
async function collectEngineFindings(text, engines, options, emit) {
|
|
136
|
+
const projection = engines.length ? projectText(text) : undefined;
|
|
137
|
+
for (const engine of engines) {
|
|
138
|
+
if (engine !== secretlintEngine && !options.detectors?.includes(engine)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const finding of await detect(
|
|
142
|
+
engine,
|
|
143
|
+
text,
|
|
144
|
+
options.maxFindings ?? 100000,
|
|
145
|
+
options
|
|
146
|
+
)) {
|
|
147
|
+
emit(finding);
|
|
148
|
+
}
|
|
149
|
+
await engineFindings(engine, projection, options, (finding) => {
|
|
150
|
+
if (builtInEnabled(finding, options)) {
|
|
151
|
+
emit(finding);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
127
157
|
async function analyzeStructured(
|
|
128
158
|
text,
|
|
129
159
|
initial,
|
|
@@ -140,7 +170,14 @@ async function analyzeStructured(
|
|
|
140
170
|
batch.text,
|
|
141
171
|
[],
|
|
142
172
|
true,
|
|
143
|
-
|
|
173
|
+
{
|
|
174
|
+
...options,
|
|
175
|
+
detectors: [],
|
|
176
|
+
properNames: undefined,
|
|
177
|
+
documentEntities: options.properNames
|
|
178
|
+
? false
|
|
179
|
+
: options.documentEntities,
|
|
180
|
+
},
|
|
144
181
|
nativeFindings === undefined ? undefined : []
|
|
145
182
|
),
|
|
146
183
|
options
|
|
@@ -152,7 +189,11 @@ async function analyzeStructured(
|
|
|
152
189
|
view.text,
|
|
153
190
|
[],
|
|
154
191
|
true,
|
|
155
|
-
{
|
|
192
|
+
{
|
|
193
|
+
...options,
|
|
194
|
+
structured: undefined,
|
|
195
|
+
properNames: view.prose ? options.properNames : undefined,
|
|
196
|
+
},
|
|
156
197
|
nativeFindings === undefined ? undefined : []
|
|
157
198
|
);
|
|
158
199
|
for (const finding of detected) {
|
|
@@ -189,9 +230,7 @@ export function createSanitizer(options = {}) {
|
|
|
189
230
|
}
|
|
190
231
|
}
|
|
191
232
|
const engines =
|
|
192
|
-
options.secretlint === false || options.knownOnly
|
|
193
|
-
? []
|
|
194
|
-
: [{ detect: secretlintDetect }];
|
|
233
|
+
options.secretlint === false || options.knownOnly ? [] : [secretlintEngine];
|
|
195
234
|
if (!options.knownOnly) {
|
|
196
235
|
engines.push(...(options.detectors ?? []));
|
|
197
236
|
}
|
|
@@ -220,20 +259,15 @@ export function createSanitizer(options = {}) {
|
|
|
220
259
|
}
|
|
221
260
|
// Native inspection performs its own normalization. Additional views are
|
|
222
261
|
// needed here only when a required external engine is enabled.
|
|
223
|
-
|
|
224
|
-
for (const engine of engines) {
|
|
225
|
-
for (const finding of await detect(
|
|
226
|
-
engine,
|
|
227
|
-
text,
|
|
228
|
-
options.maxFindings ?? 100000
|
|
229
|
-
)) {
|
|
230
|
-
emit(finding);
|
|
231
|
-
}
|
|
232
|
-
await engineFindings(engine, projection, options, emit);
|
|
233
|
-
}
|
|
262
|
+
await collectEngineFindings(text, engines, options, emit);
|
|
234
263
|
const detected =
|
|
235
264
|
nativeFindings === undefined
|
|
236
|
-
? inspect(text, {
|
|
265
|
+
? inspect(text, {
|
|
266
|
+
...options,
|
|
267
|
+
structured: undefined,
|
|
268
|
+
detectors: [],
|
|
269
|
+
findings,
|
|
270
|
+
})
|
|
237
271
|
: reviewFindings(text, [...nativeFindings, ...findings], options);
|
|
238
272
|
return options.verifyPublic
|
|
239
273
|
? verifyFindings(text, detected, options.verifyPublic)
|
|
@@ -287,16 +321,17 @@ export function createSanitizer(options = {}) {
|
|
|
287
321
|
findings,
|
|
288
322
|
fullRedaction ? fullOptions : options,
|
|
289
323
|
(decoded, recursiveOptions) =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
298
|
-
: Promise.resolve({ text: '[REDACTED]' })
|
|
324
|
+
sanitizeNested(
|
|
325
|
+
sanitizeText,
|
|
326
|
+
decoded,
|
|
327
|
+
depth,
|
|
328
|
+
fullRedaction,
|
|
329
|
+
recursiveOptions
|
|
330
|
+
)
|
|
299
331
|
);
|
|
332
|
+
if (await customResidual(result.text, options, plain, analyze)) {
|
|
333
|
+
throw failure('ERR_RESIDUAL');
|
|
334
|
+
}
|
|
300
335
|
const sanitized = fullRedaction
|
|
301
336
|
? result.text
|
|
302
337
|
: (
|
|
@@ -305,9 +340,13 @@ export function createSanitizer(options = {}) {
|
|
|
305
340
|
findings,
|
|
306
341
|
fullOptions,
|
|
307
342
|
(decoded, recursiveOptions) =>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
sanitizeNested(
|
|
344
|
+
sanitizeText,
|
|
345
|
+
decoded,
|
|
346
|
+
depth,
|
|
347
|
+
true,
|
|
348
|
+
recursiveOptions
|
|
349
|
+
)
|
|
311
350
|
)
|
|
312
351
|
).text;
|
|
313
352
|
// Verification at the publication boundary runs every enabled engine.
|
|
@@ -320,15 +359,21 @@ export function createSanitizer(options = {}) {
|
|
|
320
359
|
sanitizeText(result.text, depth, fullRedaction, plain, next, nextRound)
|
|
321
360
|
);
|
|
322
361
|
}
|
|
323
|
-
|
|
324
|
-
verifyStructured(result.text, options);
|
|
325
|
-
}
|
|
362
|
+
verifyStructuredResult(result.text, options, plain);
|
|
326
363
|
if (await placeholderResidual(result.text, options, plain, analyze)) {
|
|
327
364
|
throw failure('ERR_RESIDUAL');
|
|
328
365
|
}
|
|
329
366
|
return { ...result, findings: auditFaked(findings, options, result) };
|
|
330
367
|
}
|
|
331
368
|
}
|
|
369
|
+
async function customResidual(text, options, plain, analyze) {
|
|
370
|
+
return (
|
|
371
|
+
hasCustomTransforms(options) &&
|
|
372
|
+
(await analyze(text, [], plain, options)).some((f) =>
|
|
373
|
+
actionable(f, options)
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
}
|
|
332
377
|
|
|
333
378
|
function guarded(callback, cleanup = () => {}) {
|
|
334
379
|
return async (...args) => {
|
|
@@ -399,3 +444,15 @@ async function verifyFindings(text, findings, verifier) {
|
|
|
399
444
|
}
|
|
400
445
|
return retained;
|
|
401
446
|
}
|
|
447
|
+
|
|
448
|
+
function sanitizeNested(sanitizeText, decoded, depth, fullRedaction, options) {
|
|
449
|
+
return depth < 2
|
|
450
|
+
? sanitizeText(decoded, depth + 1, fullRedaction, true, options)
|
|
451
|
+
: Promise.resolve({ text: '[REDACTED]' });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function verifyStructuredResult(text, options, plain) {
|
|
455
|
+
if (options.structured && !plain) {
|
|
456
|
+
verifyStructured(text, options);
|
|
457
|
+
}
|
|
458
|
+
}
|