@link-foundation/sensitive-data-sanitizer 0.19.2 → 0.19.3
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 +6 -0
- package/docs/API.md +1 -1
- package/docs/COVERAGE.md +19 -1
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/strict-attributes.js +71 -0
- package/src/strict.js +2 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6bdaf11: Recognize additional inflected special-category attributes and unfamiliar values after explicit category cues in strict mode. Require personal context for ambiguous English attributes so operational logs and JSON statuses stay intact.
|
|
8
|
+
|
|
3
9
|
## 0.19.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/docs/API.md
CHANGED
|
@@ -17,7 +17,7 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
|
|
|
17
17
|
| `compileKnown(...lists)` | Copy, freeze, merge and precompile caller-known personal values |
|
|
18
18
|
| `auditGitHistory(repository, options)` | Read-only audit of local reachable text objects |
|
|
19
19
|
|
|
20
|
-
`redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
20
|
+
`redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. The optional `strict-attributes` debug event reports only detected and suppressed match counts. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
21
21
|
|
|
22
22
|
## Options
|
|
23
23
|
|
package/docs/COVERAGE.md
CHANGED
|
@@ -476,7 +476,25 @@ proper-name mode. Romashka Inc keeps its existing legal-form treatment.
|
|
|
476
476
|
`strict: true` independently opts into personal attributes (gender, family,
|
|
477
477
|
ethnicity, religion, health, criminal/political status) and bare dates, ages,
|
|
478
478
|
passport/mobile/postal shapes. It is intended for stored personal prose and can
|
|
479
|
-
remove ordinary uses of
|
|
479
|
+
remove ordinary uses of unambiguous category words. Bounded Russian case/stem
|
|
480
|
+
rules cover disability, ethnicity, religion, diagnoses, intimate life and criminal
|
|
481
|
+
status, including `инвалидом`, `армянин`, `еврейка`, `казах`, `христианин`, `иудей`,
|
|
482
|
+
`эпилепсией`, `астма`, `онкологию`, `туберкулез`, `психиатра`, `депрессия`,
|
|
483
|
+
`гепатит`, `гей` and `отбывал наказание`. English coverage includes `epilepsy`,
|
|
484
|
+
`gay` and `Armenian`. Explicit category cues such as `по национальности X`,
|
|
485
|
+
`исповедует X`, `болеет X`, `на учете у X`, `diagnosed with X` and `ethnicity: X`
|
|
486
|
+
protect one following word (including hyphenated words) up to 64 characters,
|
|
487
|
+
including unfamiliar values outside the lexicon. These cues stay on one line.
|
|
488
|
+
Ambiguous English `disabled`, `single`, `man`, `men`, `russian`
|
|
489
|
+
and `asian` require an adjacent personal cue (pronoun/person role, personal
|
|
490
|
+
label, or following person role). Thus `he is disabled`, `candidate is single`
|
|
491
|
+
and `gender: male` redact, while `the module is disabled`, `single run finished`,
|
|
492
|
+
`single-flight lock acquired`, `man page missing`, `locale russian selected`
|
|
493
|
+
and `asian region endpoint` remain intact. Bare operational JSON statuses
|
|
494
|
+
`disabled`, `single` and `Russian` also remain intact; explicit personal labels
|
|
495
|
+
and caller-declared known personal values still redact. A cue in another clause
|
|
496
|
+
or string value does not make an ambiguous word personal.
|
|
497
|
+
It preserves ISO timestamps, work durations
|
|
480
498
|
(including `Стаж: 5 лет`, `работал там 5 лет`, and duration cues from issue 87),
|
|
481
499
|
and parenthesized durations after known technologies such as `Kubernetes (5 лет)`
|
|
482
500
|
and `Power BI (5 лет)`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"description": "Offline sensitive-text sanitization with credential, multilingual personal-data and scanner union detection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.d.ts
CHANGED
|
@@ -118,6 +118,7 @@ export interface SanitizerOptions {
|
|
|
118
118
|
}
|
|
119
119
|
/** Value-free diagnostics; enabled only when a debug callback is supplied. */
|
|
120
120
|
export type DebugEvent =
|
|
121
|
+
| { event: 'strict-attributes'; detected: number; suppressed: number }
|
|
121
122
|
| { event: 'detected'; count: number }
|
|
122
123
|
| { event: 'document-experience' | 'proper-name-prose'; count: number }
|
|
123
124
|
| { event: 'residual-redaction'; round: number }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { collectMatches } from './detection.js';
|
|
2
|
+
|
|
3
|
+
const attributes =
|
|
4
|
+
/(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|male|female|married|divorced|pregnant|pregnancy|disability|christian|muslim|jewish|orthodox|hispanic|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
|
|
5
|
+
const additionalAttributes =
|
|
6
|
+
/(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
|
|
7
|
+
|
|
8
|
+
// Bounded suffixes cover Russian cases, plurals and adjectival forms without
|
|
9
|
+
// turning a stem into an unbounded substring search. Category cues below cover
|
|
10
|
+
// unfamiliar diagnoses, nationalities and religions outside this lexicon.
|
|
11
|
+
const categoryForms =
|
|
12
|
+
/(?<![\p{L}\p{N}_])(?:(?:диабет|католик|католич|буддист|атеист|белорус|православн|беременн|судим)[\p{L}]{0,8}|татар(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|ы|ам|ами|ах)?|мусульман(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|е|ам|ами|ах)?|украин(?:ец|ца|цу|цем|це|цы|цев|цам|цами|цах|к[аиуе]|кой)|инвалид(?:а|у|ом|е|ы|ов|ам|ами|ах|к[аиу]|кой)?|армян(?:ин|ина|ину|ином|ине|е|ам|ами|ах|ка|ки|ке|ку|кой)|евре(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|казах(?:а|у|ом|е|и|ов|ам|ами|ах|к[аиуе]|кой)?|христиан(?:ин|ина|ину|ином|ине|е|ам|ами|ах|к[аиуе]|кой)|иуде(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|ислам[\p{L}]{0,5}|эпилепси[\p{L}]{1,5}|астм[\p{L}]{1,5}|онкологи[\p{L}]{1,5}|туберкул[её]з[\p{L}]{0,5}|психиатр[\p{L}]{0,6}|депресси[\p{L}]{1,5}|гепатит[\p{L}]{0,5}|гомосексуал[\p{L}]{0,7}|лесбиян[\p{L}]{1,5}|ге(?:й|я|ю|ем|и|ев|ям|ями|ях)|нетрадиционн[\p{L}]{2,3}[ \t]+ориентаци[\p{L}]{1,3}|отбыва(?:л[аи]?|ет|ют|вш[\p{L}]{2,3})[ \t]+наказани[\p{L}]{1,3}|ВИЧ-инфицирован[\p{L}]{0,4}|epilepsy|asthma|tuberculosis|hepatitis|depression|gay|lesbian|homosexual|bisexual|armenian)(?![\p{L}\p{N}_])/giu;
|
|
13
|
+
const categoryCue =
|
|
14
|
+
/(?<![\p{L}\p{N}_])(?:по[ \t]+национальности|национальность|этничность|вероисповедание|религия|диагноз|исповеду(?:ет|ют|ю|ешь)|боле(?:ет|ют|ю|ешь)|страда(?:ет|ют|ю|ешь)[ \t]+от|на[ \t]+уч[её]те[ \t]+у|diagnosed[ \t]+with|suffers[ \t]+from|nationality|ethnicity|religion|diagnosis)(?:["']?[ \t]*[:=][ \t]*|[ \t]+)["']?([\p{L}][\p{L}\p{M}-]{0,63})(?![\p{L}\p{M}\p{N}_])/giu;
|
|
15
|
+
const ambiguous =
|
|
16
|
+
/(?<![\p{L}\p{N}_])(?:disabled|single|man|men|russian|asian)(?![\p{L}\p{N}_])/giu;
|
|
17
|
+
|
|
18
|
+
function personalContext(text, match) {
|
|
19
|
+
const prefix = text.slice(Math.max(0, match.index - 128), match.index);
|
|
20
|
+
const suffix = text.slice(
|
|
21
|
+
match.index + match[0].length,
|
|
22
|
+
match.index + match[0].length + 64
|
|
23
|
+
);
|
|
24
|
+
// These continuations remain technical even after a pronoun ("it is a
|
|
25
|
+
// single-flight lock"). No document-wide cue can authorize another clause.
|
|
26
|
+
if (
|
|
27
|
+
/^(?:-flight\b|[ \t]+(?:page|pages|run|worker|thread|lock|region|endpoint|locale|module|feature|flag)\b)/iu.test(
|
|
28
|
+
suffix
|
|
29
|
+
)
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
/(?<![\p{L}\p{N}_])(?:i|he|she|we|they|candidate|applicant|patient|person|employee)(?:[ \t]+(?:am|is|are|was|were|identifies[ \t]+as))?(?:[ \t]+(?:a|an))?[ \t]+$/iu.test(
|
|
35
|
+
prefix
|
|
36
|
+
) ||
|
|
37
|
+
/(?<![\p{L}\p{N}_])(?:gender|sex|nationality|ethnicity|marital[ \t_-]*status|disability)(?:["'][ \t]*)?[ \t]*[:=][ \t]*["']?$/iu.test(
|
|
38
|
+
prefix
|
|
39
|
+
) ||
|
|
40
|
+
/^[ \t]+(?:candidate|applicant|patient|person|people|employee)(?![\p{L}\p{N}_])/iu.test(
|
|
41
|
+
suffix
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function detectStrictAttributes(text, emit, options) {
|
|
47
|
+
let detected = 0,
|
|
48
|
+
suppressed = 0;
|
|
49
|
+
const details = {
|
|
50
|
+
type: 'PERSONAL_ATTRIBUTE',
|
|
51
|
+
rule: 'strict-attribute',
|
|
52
|
+
category: 'personal',
|
|
53
|
+
confidence: 0.9,
|
|
54
|
+
};
|
|
55
|
+
const report = (finding) => {
|
|
56
|
+
detected++;
|
|
57
|
+
emit(finding);
|
|
58
|
+
};
|
|
59
|
+
for (const pattern of [attributes, additionalAttributes, categoryForms]) {
|
|
60
|
+
collectMatches(text, pattern, report, details);
|
|
61
|
+
}
|
|
62
|
+
collectMatches(text, categoryCue, report, details, 1);
|
|
63
|
+
collectMatches(text, ambiguous, report, details, 0, (_value, match) => {
|
|
64
|
+
const accepted = personalContext(text, match);
|
|
65
|
+
if (!accepted) {
|
|
66
|
+
suppressed++;
|
|
67
|
+
}
|
|
68
|
+
return accepted;
|
|
69
|
+
});
|
|
70
|
+
options.debug?.({ event: 'strict-attributes', detected, suppressed });
|
|
71
|
+
}
|
package/src/strict.js
CHANGED
|
@@ -3,12 +3,9 @@ import { months } from './personal-context.js';
|
|
|
3
3
|
import { isDocumentTechnology } from './document-experience.js';
|
|
4
4
|
import { placeholderValues } from './placeholders.js';
|
|
5
5
|
import { generatedValues } from './fake.js';
|
|
6
|
+
import { detectStrictAttributes } from './strict-attributes.js';
|
|
6
7
|
|
|
7
8
|
const boundary = '[\\p{L}\\p{N}_]';
|
|
8
|
-
const attributes =
|
|
9
|
-
/(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|man|men|male|female|married|divorced|pregnant|pregnancy|disabled|disability|christian|muslim|jewish|orthodox|russian|hispanic|asian|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
|
|
10
|
-
const additionalAttributes =
|
|
11
|
-
/(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|single|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
|
|
12
9
|
|
|
13
10
|
function isDuration(text, match, replacements) {
|
|
14
11
|
// Keep cues on the same clause; an age on the next line is independent.
|
|
@@ -57,14 +54,7 @@ export function detectStrict(text, emit, options) {
|
|
|
57
54
|
if (!options.strict) {
|
|
58
55
|
return;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
collectMatches(text, pattern, emit, {
|
|
62
|
-
type: 'PERSONAL_ATTRIBUTE',
|
|
63
|
-
rule: 'strict-attribute',
|
|
64
|
-
category: 'personal',
|
|
65
|
-
confidence: 0.9,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
57
|
+
detectStrictAttributes(text, emit, options);
|
|
68
58
|
const date = new RegExp(
|
|
69
59
|
`(?<!${boundary})(?:\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[./]\\d{1,2}[./]\\d{4}|\\d{1,2}[ \\t]+(?:${months})[ \\t]+\\d{4})(?!${boundary}|[Tt]\\d)`,
|
|
70
60
|
'giu'
|